diff --git a/.github/CLAUDE.md b/.github/CLAUDE.md
index 05c925ea..be5815b0 100644
--- a/.github/CLAUDE.md
+++ b/.github/CLAUDE.md
@@ -265,7 +265,7 @@ LogExpert/
- UI components in `LogExpert.UI` project
- Follow existing High DPI patterns (no AutoScale on controls)
-- Test with both light and dark mode (see `SetDarkMode()` in Program.cs)
+- Test with both light and dark mode (see `SetColorMode()` in Program.cs; the `ColorMode` preference is Light/Dark/System)
- Use localization resources from `LogExpert.Resources` project
- Windows Forms designer files: `*.designer.cs`
diff --git a/src/LogExpert.Core/Config/ColorMode.cs b/src/LogExpert.Core/Config/ColorMode.cs
new file mode 100644
index 00000000..004c1d01
--- /dev/null
+++ b/src/LogExpert.Core/Config/ColorMode.cs
@@ -0,0 +1,27 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace LogExpert.Core.Config;
+
+///
+/// Color scheme the application applies at startup (issue #698).
+///
+[Serializable]
+[JsonConverter(typeof(StringEnumConverter))]
+public enum ColorMode
+{
+ ///
+ /// Forced light mode, ignoring the Windows theme (maps to SystemColorMode.Classic).
+ ///
+ Light = 0,
+
+ ///
+ /// Forced dark mode, ignoring the Windows theme.
+ ///
+ Dark = 1,
+
+ ///
+ /// Follow the Windows theme.
+ ///
+ System = 2,
+}
diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs
index 61668708..ff4dd1bd 100644
--- a/src/LogExpert.Core/Config/Preferences.cs
+++ b/src/LogExpert.Core/Config/Preferences.cs
@@ -79,7 +79,44 @@ public List HilightGroupList
public bool AskForClose { get; set; }
- public bool DarkMode { get; set; }
+ ///
+ /// Color scheme applied at startup: forced light, forced dark, or follow the Windows theme.
+ /// Takes effect after a restart. Replaces the old bool DarkMode setting (issue #698).
+ ///
+ public ColorMode ColorMode
+ {
+ get => _colorMode;
+ set
+ {
+ _colorMode = value;
+ _colorModeAssigned = true;
+ }
+ }
+
+ private ColorMode _colorMode = ColorMode.Light;
+
+ private bool _colorModeAssigned;
+
+ ///
+ /// Legacy property for backward compatibility with old settings files that stored the color scheme as the bool
+ /// "DarkMode". true maps to , false to — an unchecked
+ /// box meant forced light, not follow-OS (issue #698). An explicit value always wins.
+ /// This setter redirects data on load; the property is never written back.
+ ///
+ [Obsolete("This property exists only for backward compatibility with old settings files. Use ColorMode instead. This will be removed with version 1.50")]
+ [Newtonsoft.Json.JsonProperty("DarkMode", DefaultValueHandling = Newtonsoft.Json.DefaultValueHandling.Ignore, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
+ [System.Text.Json.Serialization.JsonIgnore]
+ public bool? DarkMode
+ {
+ get => null; // Always return null so Newtonsoft.Json won't serialize this property
+ set
+ {
+ if (!_colorModeAssigned && value.HasValue)
+ {
+ _colorMode = value.Value ? ColorMode.Dark : ColorMode.Light;
+ }
+ }
+ }
[Obsolete("This setting is no longer used and will be removed in version 1.50. The 'UseLegacyReader' now works with ReaderType.Legacy")]
[System.Text.Json.Serialization.JsonIgnore]
diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs
index 1c400f52..22218342 100644
--- a/src/LogExpert.Resources/Resources.Designer.cs
+++ b/src/LogExpert.Resources/Resources.Designer.cs
@@ -5194,11 +5194,11 @@ public static string SettingsDialog_UI_CheckBox_checkBoxControlCharsItalic {
}
///
- /// Looks up a localized string similar to Dark Mode (restart required).
+ /// Looks up a localized string similar to Color mode (restart required).
///
- public static string SettingsDialog_UI_CheckBox_checkBoxDarkMode {
+ public static string SettingsDialog_UI_Label_labelColorMode {
get {
- return ResourceManager.GetString("SettingsDialog_UI_CheckBox_checkBoxDarkMode", resourceCulture);
+ return ResourceManager.GetString("SettingsDialog_UI_Label_labelColorMode", resourceCulture);
}
}
diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx
index e0e19567..a83dac05 100644
--- a/src/LogExpert.Resources/Resources.de.resx
+++ b/src/LogExpert.Resources/Resources.de.resx
@@ -976,8 +976,8 @@ Damit die anderen Fenster beim Auswählen einer Zeile (Mausklick oder Pfeiltaste
Dem Ende folgen aktivieren
-
- Dark Mode (neustart benötigt)
+
+ Farbmodus (Neustart benötigt)
Fragen vor dem schließen des Tabs
diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx
index a08b755c..22abd07a 100644
--- a/src/LogExpert.Resources/Resources.resx
+++ b/src/LogExpert.Resources/Resources.resx
@@ -863,8 +863,8 @@ Checked tools will appear in the icon bar. All other tools are available in the
Ask before closing tabs
-
- Dark Mode (restart required)
+
+ Color mode (restart required)
Follow tail enabled
diff --git a/src/LogExpert.Resources/Resources.zh-CN.resx b/src/LogExpert.Resources/Resources.zh-CN.resx
index f4af56f5..10233076 100644
--- a/src/LogExpert.Resources/Resources.zh-CN.resx
+++ b/src/LogExpert.Resources/Resources.zh-CN.resx
@@ -760,8 +760,8 @@
关闭标签页前询问
-
- 深色模式(需重启生效)
+
+ 颜色模式(需重启生效)
启用跟随尾部
diff --git a/src/LogExpert.Tests/ConfigManagerTests/PreferencesColorModeTests.cs b/src/LogExpert.Tests/ConfigManagerTests/PreferencesColorModeTests.cs
new file mode 100644
index 00000000..fa4e3e1d
--- /dev/null
+++ b/src/LogExpert.Tests/ConfigManagerTests/PreferencesColorModeTests.cs
@@ -0,0 +1,79 @@
+using LogExpert.Core.Config;
+
+using Newtonsoft.Json;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.ConfigManagerTests;
+
+[TestFixture]
+public class PreferencesColorModeTests
+{
+ [Test]
+ public void Deserialize_LegacyDarkModeTrue_MapsToDark ()
+ {
+ const string legacyJson = "{\"DarkMode\": true}";
+
+ var prefs = JsonConvert.DeserializeObject(legacyJson);
+
+ Assert.That(prefs, Is.Not.Null);
+ Assert.That(prefs!.ColorMode, Is.EqualTo(ColorMode.Dark));
+ }
+
+ [Test]
+ public void Deserialize_LegacyDarkModeFalse_MapsToLight ()
+ {
+ // Unchecked "Dark Mode" meant forced light, not follow-OS (issue #698).
+ const string legacyJson = "{\"DarkMode\": false}";
+
+ var prefs = JsonConvert.DeserializeObject(legacyJson);
+
+ Assert.That(prefs, Is.Not.Null);
+ Assert.That(prefs!.ColorMode, Is.EqualTo(ColorMode.Light));
+ }
+
+ [Test]
+ public void Deserialize_NoColorSetting_DefaultsToLight ()
+ {
+ const string legacyJson = "{}";
+
+ var prefs = JsonConvert.DeserializeObject(legacyJson);
+
+ Assert.That(prefs, Is.Not.Null);
+ Assert.That(prefs!.ColorMode, Is.EqualTo(ColorMode.Light));
+ }
+
+ [Test]
+ public void Deserialize_ColorModeString_ReadsEnum ()
+ {
+ const string json = "{\"ColorMode\": \"System\"}";
+
+ var prefs = JsonConvert.DeserializeObject(json);
+
+ Assert.That(prefs, Is.Not.Null);
+ Assert.That(prefs!.ColorMode, Is.EqualTo(ColorMode.System));
+ }
+
+ [Test]
+ public void Deserialize_ColorModeWinsOverLegacyDarkMode ()
+ {
+ // A file that carries both keys must obey the new one, regardless of key order.
+ const string json = "{\"ColorMode\": \"Light\", \"DarkMode\": true}";
+
+ var prefs = JsonConvert.DeserializeObject(json);
+
+ Assert.That(prefs, Is.Not.Null);
+ Assert.That(prefs!.ColorMode, Is.EqualTo(ColorMode.Light));
+ }
+
+ [Test]
+ public void Serialize_WritesColorModeAsString_AndNoLegacyDarkMode ()
+ {
+ var prefs = new Preferences { ColorMode = ColorMode.System };
+
+ var json = JsonConvert.SerializeObject(prefs);
+
+ Assert.That(json, Does.Contain("\"ColorMode\":\"System\""));
+ Assert.That(json, Does.Not.Contain("\"DarkMode\""));
+ }
+}
diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
index cfd9aa09..50ac8db0 100644
--- a/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
+++ b/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
@@ -41,7 +41,8 @@ private void InitializeComponent ()
groupBoxDefaults = new GroupBox();
labelLanguage = new Label();
comboBoxLanguage = new ComboBox();
- checkBoxDarkMode = new CheckBox();
+ labelColorMode = new Label();
+ comboBoxColorMode = new ComboBox();
checkBoxFollowTail = new CheckBox();
checkBoxColumnFinder = new CheckBox();
checkBoxSyncFilter = new CheckBox();
@@ -495,7 +496,8 @@ private void InitializeComponent ()
//
groupBoxDefaults.Controls.Add(labelLanguage);
groupBoxDefaults.Controls.Add(comboBoxLanguage);
- groupBoxDefaults.Controls.Add(checkBoxDarkMode);
+ groupBoxDefaults.Controls.Add(labelColorMode);
+ groupBoxDefaults.Controls.Add(comboBoxColorMode);
groupBoxDefaults.Controls.Add(checkBoxFollowTail);
groupBoxDefaults.Controls.Add(checkBoxColumnFinder);
groupBoxDefaults.Controls.Add(checkBoxSyncFilter);
@@ -529,17 +531,26 @@ private void InitializeComponent ()
comboBoxLanguage.Size = new Size(177, 23);
comboBoxLanguage.TabIndex = 9;
toolTip.SetToolTip(comboBoxLanguage, "Userinterface language");
- //
- // checkBoxDarkMode
- //
- checkBoxDarkMode.AutoSize = true;
- checkBoxDarkMode.Location = new Point(9, 144);
- checkBoxDarkMode.Margin = new Padding(4);
- checkBoxDarkMode.Name = "checkBoxDarkMode";
- checkBoxDarkMode.Size = new Size(175, 19);
- checkBoxDarkMode.TabIndex = 6;
- checkBoxDarkMode.Text = "Dark Mode (restart required)";
- checkBoxDarkMode.UseVisualStyleBackColor = true;
+ //
+ // labelColorMode
+ //
+ labelColorMode.AutoSize = true;
+ labelColorMode.Location = new Point(9, 147);
+ labelColorMode.Margin = new Padding(4, 0, 4, 0);
+ labelColorMode.Name = "labelColorMode";
+ labelColorMode.Size = new Size(160, 15);
+ labelColorMode.TabIndex = 18;
+ labelColorMode.Text = "Color mode (restart required)";
+ //
+ // comboBoxColorMode
+ //
+ comboBoxColorMode.DropDownStyle = ComboBoxStyle.DropDownList;
+ comboBoxColorMode.FormattingEnabled = true;
+ comboBoxColorMode.Location = new Point(204, 144);
+ comboBoxColorMode.Margin = new Padding(4, 5, 4, 5);
+ comboBoxColorMode.Name = "comboBoxColorMode";
+ comboBoxColorMode.Size = new Size(177, 23);
+ comboBoxColorMode.TabIndex = 6;
//
// checkBoxFollowTail
//
@@ -2280,7 +2291,8 @@ private void InitializeComponent ()
private System.Windows.Forms.CheckBox checkBoxPortableMode;
private System.Windows.Forms.RadioButton radioButtonSessionApplicationStartupDir;
private System.Windows.Forms.CheckBox checkBoxShowErrorMessageOnlyOneInstance;
- private System.Windows.Forms.CheckBox checkBoxDarkMode;
+ private System.Windows.Forms.Label labelColorMode;
+ private System.Windows.Forms.ComboBox comboBoxColorMode;
private System.Windows.Forms.NumericUpDown upDownMaximumLineLength;
private System.Windows.Forms.Label labelMaximumLineLength;
private System.Windows.Forms.Label labelWarningMaximumLineLength;
diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
index 4620e18d..d32f0801 100644
--- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs
+++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
@@ -185,7 +185,7 @@ private void FillDialog ()
FillPortableMode();
- checkBoxDarkMode.Checked = Preferences.DarkMode;
+ FillColorModeList();
checkBoxTimestamp.Checked = Preferences.TimestampControl;
checkBoxSyncFilter.Checked = Preferences.FilterSync;
checkBoxFilterTail.Checked = Preferences.FilterTail;
@@ -325,6 +325,16 @@ private void FillReaderTypeList ()
comboBoxReaderType.SelectedItem = Preferences.ReaderType;
}
+ private void FillColorModeList ()
+ {
+ foreach (var colorMode in Enum.GetValues().Where(cm => !comboBoxColorMode.Items.Contains(cm)))
+ {
+ _ = comboBoxColorMode.Items.Add(colorMode);
+ }
+
+ comboBoxColorMode.SelectedItem = Preferences.ColorMode;
+ }
+
internal void FillPortableMode ()
{
// Detach the handler while syncing the checkbox from preferences: CheckedChanged also
@@ -818,7 +828,7 @@ private void OnBtnOkClick (object sender, EventArgs e)
Preferences.MaximumFilterEntries = (int)upDownMaximumFilterEntries.Value;
Preferences.MaximumFilterEntriesDisplayed = (int)upDownMaximumFilterEntriesDisplayed.Value;
Preferences.ShowErrorMessageAllowOnlyOneInstances = checkBoxShowErrorMessageOnlyOneInstance.Checked;
- Preferences.DarkMode = checkBoxDarkMode.Checked;
+ Preferences.ColorMode = comboBoxColorMode.SelectedItem is ColorMode colorMode ? colorMode : ColorMode.Light;
Preferences.MaxLineLength = (int)upDownMaximumLineLength.Value;
Preferences.MaxDisplayLength = Math.Min((int)upDownMaxDisplayLength.Value, (int)upDownMaximumLineLength.Value);
diff --git a/src/LogExpert/Program.cs b/src/LogExpert/Program.cs
index 2122957f..220a5134 100644
--- a/src/LogExpert/Program.cs
+++ b/src/LogExpert/Program.cs
@@ -130,7 +130,7 @@ private static void Main (string[] args)
_ = PluginRegistry.PluginRegistry.Create(ConfigManager.Instance.ActiveConfigDir, ConfigManager.Instance.Settings.Preferences.PollingInterval);
SetCulture();
- SetDarkMode();
+ SetColorMode();
ColumnizerLib.Column.SetMaxDisplayLength(ConfigManager.Instance.Settings.Preferences.MaxDisplayLength);
@@ -248,17 +248,14 @@ or ArgumentNullException
}
[SupportedOSPlatform("windows")]
- private static void SetDarkMode ()
+ private static void SetColorMode ()
{
- var darkModeEnabled = ConfigManager.Instance.Settings.Preferences.DarkMode;
- if (darkModeEnabled)
+ Application.SetColorMode(ConfigManager.Instance.Settings.Preferences.ColorMode switch
{
- Application.SetColorMode(SystemColorMode.Dark);
- }
- else
- {
- Application.SetColorMode(SystemColorMode.System);
- }
+ ColorMode.Dark => SystemColorMode.Dark,
+ ColorMode.System => SystemColorMode.System,
+ _ => SystemColorMode.Classic,
+ });
}
[SupportedOSPlatform("windows")]