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 716e4f67c..98f18d894 100644 --- a/src/LogExpert.Core/Helpers/EncodingRegistry.cs +++ b/src/LogExpert.Core/Helpers/EncodingRegistry.cs @@ -27,6 +27,58 @@ namespace LogExpert.Core.Helpers; /// 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. + /// + /// + /// 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 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 830b727ae..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 ANSI. - /// - public static string LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem { - get { - return ResourceManager.GetString("LogTabWindow_UI_ToolStripMenuItem_encodingANSIToolStripMenuItem", resourceCulture); - } - } - - /// - /// 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 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 162fb6eec..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 - - - ANSI - - - ISO-8859-1 - - - UTF8 - - - Unicode - +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index 5df529c37..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 - - - ANSI - - - ISO-8859-1 - - - UTF8 - - - Unicode - +00:00:00.000 diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx index b3ea6c935..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 - - - 统一码 - +00:00:00.000 diff --git a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs index b94430323..c01871722 100644 --- a/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs +++ b/src/LogExpert.Tests/Dialogs/SettingsDialogEncodingListTests.cs @@ -8,122 +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")] - 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/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/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/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..e499cce92 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; @@ -66,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("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" }); + EncodingMenuBuilder.Fill(_encodingMenu, (_, _) => { }); _ = _viewMenu.DropDownItems.Add(_encodingMenu); var timeshiftMenu = new ToolStripMenuItem("Timeshift") { Name = "timeshiftToolStripMenuItem" }; @@ -156,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] @@ -175,8 +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); + } + + /// + /// 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_AnyUtf8Instance_ChecksTheUtf8Row () + { + var utf8Row = new[] { EncodingMenuBuilder.RowName(Encoding.UTF8) }; + + Assert.Multiple(() => + { + foreach (var utf8 in new[] { Encoding.UTF8, new UTF8Encoding(false), Encoding.Default }) + { + _controller.UpdateEncodingMenu(utf8); + + Assert.That(CheckedEncodingRowNames(), Is.EqualTo(utf8Row).AsCollection); + } + }); + } + + /// + /// 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_EncodingThatIsNotOffered_ChecksNothing () + { + _controller.UpdateEncodingMenu(Encoding.BigEndianUnicode); + + Assert.That(CheckedEncodingRowNames(), Is.Empty); + } + + private List CheckedEncodingRowNames () + { + 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.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..2e44ec9e8 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -141,13 +141,11 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu BackColor = Color.FromKnownColor(KnownColor.Transparent) }; - var index = buttonToolStrip.Items.IndexOfKey("toolStripButtonTail"); + // Built here rather than declared in the designer so the menu and the Preferences combo box + // cannot offer different encodings. + EncodingMenuBuilder.Fill(encodingToolStripMenuItem, OnEncodingToolStripMenuItemClick); - 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; + var index = buttonToolStrip.Items.IndexOfKey("toolStripButtonTail"); if (index != -1) { @@ -451,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; - 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; timeshiftToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_timeshiftToolStripMenuItem; timeshiftToolStripTextBox.Text = Resources.LogTabWindow_UI_ToolStripTextBox_timeshiftToolStripTextBox; copyMarkedLinesIntoNewTabToolStripMenuItem.Text = Resources.LogTabWindow_UI_ToolStripMenuItem_copyMarkedLinesIntoNewTabToolStripMenuItem; @@ -2172,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 OnANSIToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(Encoding.Default); - } - - [SupportedOSPlatform("windows")] - private void OnUTF8ToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(new UTF8Encoding(false)); - } - - [SupportedOSPlatform("windows")] - private void OnUTF16ToolStripMenuItemClick (object sender, EventArgs e) + private void OnEncodingToolStripMenuItemClick (object sender, EventArgs e) { - CurrentLogWindow?.ChangeEncoding(Encoding.Unicode); - } + var encoding = EncodingMenuBuilder.EncodingOf(sender as ToolStripItem); - [SupportedOSPlatform("windows")] - private void OnISO88591ToolStripMenuItemClick (object sender, EventArgs e) - { - CurrentLogWindow?.ChangeEncoding(Encoding.GetEncoding("iso-8859-1")); + 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 8f7bab2ae..c7d641095 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; @@ -58,11 +58,6 @@ private void InitializeComponent () columnFinderToolStripMenuItem = new ToolStripMenuItem(); ToolStripSeparator5 = new ToolStripSeparator(); encodingToolStripMenuItem = new ToolStripMenuItem(); - encodingASCIIToolStripMenuItem = new ToolStripMenuItem(); - encodingANSIToolStripMenuItem = new ToolStripMenuItem(); - encodingISO88591toolStripMenuItem = new ToolStripMenuItem(); - encodingUTF8toolStripMenuItem = new ToolStripMenuItem(); - encodingUTF16toolStripMenuItem = new ToolStripMenuItem(); ToolStripSeparator6 = new ToolStripSeparator(); timeshiftToolStripMenuItem = new ToolStripMenuItem(); timeshiftToolStripTextBox = new ToolStripTextBox(); @@ -438,57 +433,11 @@ private void InitializeComponent () // // encodingToolStripMenuItem // - encodingToolStripMenuItem.DropDownItems.AddRange(new ToolStripItem[] { encodingASCIIToolStripMenuItem, encodingANSIToolStripMenuItem, encodingISO88591toolStripMenuItem, encodingUTF8toolStripMenuItem, encodingUTF16toolStripMenuItem }); + // 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; - // - // 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; - 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; - // // ToolStripSeparator6 // ToolStripSeparator6.Name = "ToolStripSeparator6"; @@ -1142,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 encodingANSIToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingUTF8toolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem encodingUTF16toolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem reloadToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem columnizerToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; @@ -1195,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 62732f7e5..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,31 +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 - /// and Windows-1252. - /// - /// - /// 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. - /// - internal static IReadOnlyList GetAvailableEncodings () - { - return - [ - Encoding.ASCII, - Encoding.Latin1, - Encoding.UTF8, - Encoding.Unicode, - EncodingRegistry.GetEncoding(1250), - EncodingRegistry.GetEncoding(1252) - ]; - } - /// /// Populates the language selection list with available language options. /// 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/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 04cb3083d..fbe8a851c 100644 --- a/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs +++ b/src/LogExpert.UI/Services/MenuToolbarService/MenuToolbarController.cs @@ -36,12 +36,9 @@ internal sealed class MenuToolbarController : IMenuToolbarController private ToolStripMenuItem _cellSelectMenuItem; private ToolStripMenuItem _columnFinderMenuItem; - // Encoding menu items - private ToolStripMenuItem _encodingAsciiMenuItem; - private ToolStripMenuItem _encodingAnsiMenuItem; - private ToolStripMenuItem _encodingUtf8MenuItem; - private ToolStripMenuItem _encodingUtf16MenuItem; - private ToolStripMenuItem _encodingIso88591MenuItem; + // 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; // Toolbar items private ToolStripButton _bubblesButton; @@ -94,11 +91,7 @@ public void InitializeMenus (MenuStrip mainMenu, ToolStrip buttonToolbar, _columnFinderMenuItem = FindMenuItem("columnFinderToolStripMenuItem"); // Encoding menu items - _encodingAsciiMenuItem = FindMenuItem("encodingASCIIToolStripMenuItem"); - _encodingAnsiMenuItem = FindMenuItem("encodingANSIToolStripMenuItem"); - _encodingUtf8MenuItem = FindMenuItem("encodingUTF8toolStripMenuItem"); - _encodingUtf16MenuItem = FindMenuItem("encodingUTF16toolStripMenuItem"); - _encodingIso88591MenuItem = FindMenuItem("encodingISO88591toolStripMenuItem"); + _encodingMenuItem = FindMenuItem("encodingToolStripMenuItem"); // Toolbar items _bubblesButton = FindToolStripItem(_buttonToolbar, "toolStripButtonBubbles"); @@ -198,41 +191,26 @@ public void UpdateEncodingMenu (Encoding currentEncoding) return; } - // Clear all checks - SetCheckedSafe(_encodingAsciiMenuItem, false); - SetCheckedSafe(_encodingAnsiMenuItem, false); - SetCheckedSafe(_encodingUtf8MenuItem, false); - SetCheckedSafe(_encodingUtf16MenuItem, false); - SetCheckedSafe(_encodingIso88591MenuItem, false); - - if (currentEncoding == null) + if (_encodingMenuItem == null) { return; } - if (currentEncoding is ASCIIEncoding) - { - SetCheckedSafe(_encodingAsciiMenuItem, true); - } - else if (currentEncoding.Equals(Encoding.Default)) - { - SetCheckedSafe(_encodingAnsiMenuItem, true); - } - else if (currentEncoding is UTF8Encoding) - { - SetCheckedSafe(_encodingUtf8MenuItem, true); - } - else if (currentEncoding is UnicodeEncoding) - { - SetCheckedSafe(_encodingUtf16MenuItem, true); - } - else if (currentEncoding.Equals(Encoding.GetEncoding("iso-8859-1"))) + // 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; + + foreach (var row in _encodingMenuItem.DropDownItems) { - SetCheckedSafe(_encodingIso88591MenuItem, true); + if (row is ToolStripMenuItem encodingRow) + { + encodingRow.Checked = currentCodePage != null + && EncodingMenuBuilder.EncodingOf(encodingRow)?.CodePage == currentCodePage; + } } - - // Preserve existing behavior: update ANSI display name - _ = (_encodingAnsiMenuItem?.Text = Encoding.Default.HeaderName); } public void UpdateHighlightGroups (IEnumerable groups, string selectedGroup) @@ -409,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"); }