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");
}