Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
27 changes: 27 additions & 0 deletions src/LogExpert.Core/Config/ColorMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace LogExpert.Core.Config;

/// <summary>
/// Color scheme the application applies at startup (issue #698).
/// </summary>
[Serializable]
[JsonConverter(typeof(StringEnumConverter))]
public enum ColorMode
{
/// <summary>
/// Forced light mode, ignoring the Windows theme (maps to SystemColorMode.Classic).
/// </summary>
Light = 0,

/// <summary>
/// Forced dark mode, ignoring the Windows theme.
/// </summary>
Dark = 1,

/// <summary>
/// Follow the Windows theme.
/// </summary>
System = 2,
}
39 changes: 38 additions & 1 deletion src/LogExpert.Core/Config/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,44 @@ public List<HighlightGroup> HilightGroupList

public bool AskForClose { get; set; }

public bool DarkMode { get; set; }
/// <summary>
/// Color scheme applied at startup: forced light, forced dark, or follow the Windows theme.
/// Takes effect after a restart. Replaces the old bool <c>DarkMode</c> setting (issue #698).
/// </summary>
public ColorMode ColorMode
{
get => _colorMode;
set
{
_colorMode = value;
_colorModeAssigned = true;
}
}

private ColorMode _colorMode = ColorMode.Light;

private bool _colorModeAssigned;

/// <summary>
/// Legacy property for backward compatibility with old settings files that stored the color scheme as the bool
/// "DarkMode". true maps to <see cref="ColorMode.Dark"/>, false to <see cref="ColorMode.Light"/> — an unchecked
/// box meant forced light, not follow-OS (issue #698). An explicit <see cref="ColorMode"/> value always wins.
/// This setter redirects data on load; the property is never written back.
/// </summary>
[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]
Expand Down
6 changes: 3 additions & 3 deletions src/LogExpert.Resources/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,8 @@ Damit die anderen Fenster beim Auswählen einer Zeile (Mausklick oder Pfeiltaste
<data name="SettingsDialog_UI_CheckBox_checkBoxFollowTail" xml:space="preserve">
<value>Dem Ende folgen aktivieren</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxDarkMode" xml:space="preserve">
<value>Dark Mode (neustart benötigt)</value>
<data name="SettingsDialog_UI_Label_labelColorMode" xml:space="preserve">
<value>Farbmodus (Neustart benötigt)</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxAskCloseTabs" xml:space="preserve">
<value>Fragen vor dem schließen des Tabs</value>
Expand Down
4 changes: 2 additions & 2 deletions src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -863,8 +863,8 @@ Checked tools will appear in the icon bar. All other tools are available in the
<data name="SettingsDialog_UI_CheckBox_checkBoxAskCloseTabs" xml:space="preserve">
<value>Ask before closing tabs</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxDarkMode" xml:space="preserve">
<value>Dark Mode (restart required)</value>
<data name="SettingsDialog_UI_Label_labelColorMode" xml:space="preserve">
<value>Color mode (restart required)</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxFollowTail" xml:space="preserve">
<value>Follow tail enabled</value>
Expand Down
4 changes: 2 additions & 2 deletions src/LogExpert.Resources/Resources.zh-CN.resx
Original file line number Diff line number Diff line change
Expand Up @@ -760,8 +760,8 @@
<data name="SettingsDialog_UI_CheckBox_checkBoxAskCloseTabs" xml:space="preserve">
<value>关闭标签页前询问</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxDarkMode" xml:space="preserve">
<value>深色模式(需重启生效)</value>
<data name="SettingsDialog_UI_Label_labelColorMode" xml:space="preserve">
<value>颜色模式(需重启生效)</value>
</data>
<data name="SettingsDialog_UI_CheckBox_checkBoxFollowTail" xml:space="preserve">
<value>启用跟随尾部</value>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Preferences>(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<Preferences>(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<Preferences>(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<Preferences>(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<Preferences>(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\""));
}
}
40 changes: 26 additions & 14 deletions src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/LogExpert.UI/Dialogs/SettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -325,6 +325,16 @@ private void FillReaderTypeList ()
comboBoxReaderType.SelectedItem = Preferences.ReaderType;
}

private void FillColorModeList ()
{
foreach (var colorMode in Enum.GetValues<ColorMode>().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
Expand Down Expand Up @@ -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);

Expand Down
17 changes: 7 additions & 10 deletions src/LogExpert/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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")]
Expand Down
Loading