Skip to content
Closed
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
28 changes: 28 additions & 0 deletions src/LogExpert.UI.Tests/Services/LedIndicatorServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,34 @@ public void ClearDirty_ClearsStateAndRaisesCleanIcon ()
Assert.That(raisedIcon, Is.SameAs(_service.GetIcon(50, new LedState())));
}

[TestCase(false, 100)]
[TestCase(true, 170)]
public void GetInactiveLedColor_FollowsColorMode (bool darkMode, int expectedGray)
{
var color = LedIndicatorService.GetInactiveLedColor(darkMode);

Assert.Multiple(() =>
{
Assert.That(color.R, Is.EqualTo(expectedGray));
Assert.That(color.G, Is.EqualTo(expectedGray));
Assert.That(color.B, Is.EqualTo(expectedGray));
});
}

[TestCase(false, 160)]
[TestCase(true, 175)]
public void GetTailOffLedColor_FollowsColorMode (bool darkMode, int expectedGray)
{
var color = LedIndicatorService.GetTailOffLedColor(darkMode);

Assert.Multiple(() =>
{
Assert.That(color.R, Is.EqualTo(expectedGray));
Assert.That(color.G, Is.EqualTo(expectedGray));
Assert.That(color.B, Is.EqualTo(expectedGray));
});
}

private static LogWindow CreateLogWindow ()
{
var configManagerMock = new Mock<IConfigManager>();
Expand Down
90 changes: 90 additions & 0 deletions src/LogExpert.UI.Tests/Theming/DarkToolStripRendererTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Drawing.Imaging;
using System.Runtime.Versioning;

using LogExpert.UI.Theming;

using NUnit.Framework;

using WeifenLuo.WinFormsUI.Docking;

namespace LogExpert.UI.Tests.Theming;

[TestFixture]
[Apartment(ApartmentState.STA)] // Required for WinForms components
[SupportedOSPlatform("windows")]
public class DarkToolStripRendererTests
{
[Test]
public void Constructor_WithTheVS2015DarkPalette_Succeeds ()
{
// Guards the wiring in ToolStripTheme: the theme has to expose its palette before any
// window exists, otherwise the renderer cannot be built at start-up.
var theme = new VS2015DarkTheme();

var renderer = new DarkToolStripRenderer(theme.ColorPalette);

Assert.That(renderer, Is.Not.Null);
}

[Test]
public void ShouldInvertIcon_DarkMonochromeGlyph_ReturnsTrue ()
{
// The resource glyphs are black-on-transparent PNGs.
using var glyph = CreateIcon(Color.Black);

Assert.That(DarkToolStripRenderer.ShouldInvertIcon(glyph), Is.True);
}

[Test]
public void ShouldInvertIcon_ColouredIcon_ReturnsFalse ()
{
// Inverting a blue icon turned it orange; coloured icons must keep their colours.
using var icon = CreateIcon(Color.FromArgb(0, 120, 215));

Assert.That(DarkToolStripRenderer.ShouldInvertIcon(icon), Is.False);
}

[Test]
public void ShouldInvertIcon_LightMonochromeGlyph_ReturnsFalse ()
{
// A light glyph is already visible on the dark strip; inverting would make it invisible.
using var glyph = CreateIcon(Color.White);

Assert.That(DarkToolStripRenderer.ShouldInvertIcon(glyph), Is.False);
}

[Test]
public void ShouldInvertIcon_EmptyImage_ReturnsFalse ()
{
using var bitmap = new Bitmap(16, 16, PixelFormat.Format32bppArgb);

Assert.That(DarkToolStripRenderer.ShouldInvertIcon(bitmap), Is.False);
}

[Test]
public void ShouldInvertIcon_GreyscaleGlyphWithAntiAliasing_ReturnsTrue ()
{
// Real glyphs are anti-aliased, so their edge pixels are grey rather than black. The
// tolerance has to accept them; otherwise half the toolbar would stay dark.
using var bitmap = new Bitmap(16, 16, PixelFormat.Format32bppArgb);

using (var graphics = Graphics.FromImage(bitmap))
using (var brush = new SolidBrush(Color.FromArgb(90, 90, 90)))
{
graphics.FillRectangle(brush, 2, 2, 12, 12);
}

Assert.That(DarkToolStripRenderer.ShouldInvertIcon(bitmap), Is.True);
}

private static Bitmap CreateIcon (Color color)
{
var bitmap = new Bitmap(16, 16, PixelFormat.Format32bppArgb);

using var graphics = Graphics.FromImage(bitmap);
using var brush = new SolidBrush(color);
graphics.FillRectangle(brush, 2, 2, 12, 12);

return bitmap;
}
}
67 changes: 67 additions & 0 deletions src/LogExpert.UI.Tests/UI/PaintHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,73 @@ public void GetForeColorFromHighlightEntry_NoEntryInDarkMode_FallsBackToLightCol
Assert.That(IsLight(foreColor), Is.True, $"expected a light fallback color but got {foreColor}");
}

[Test]
public void GetDefaultForeColor_DarkMode_UsesTheSoftOffWhite ()
{
// 220,220,220 rather than pure white; the grid, the color combo box and the highlight
// preview have to agree on it.
var foreColor = PaintHelper.GetDefaultForeColor(darkMode: true);

Assert.That(foreColor.ToArgb(), Is.EqualTo(Color.FromArgb(220, 220, 220).ToArgb()));
}

[Test]
public void GetDefaultForeColor_LightMode_UsesBlack ()
{
var foreColor = PaintHelper.GetDefaultForeColor(darkMode: false);

Assert.That(foreColor, Is.EqualTo(Color.Black));
}

[Test]
public void GetDefaultBackColor_MatchesTheWindowBackgroundOfEachColorMode ()
{
// 32,32,32 is the surface the grid paints cells with in dark mode; the highlight
// preview has to use the same one instead of SystemColors.Window.
Assert.Multiple(() =>
{
Assert.That(PaintHelper.GetDefaultBackColor(darkMode: true), Is.EqualTo(Color.FromArgb(32, 32, 32)));
Assert.That(PaintHelper.GetDefaultBackColor(darkMode: false), Is.EqualTo(Color.White));
});
}

[Test]
public void GetForeColorFromHighlightEntry_EntryWithoutForeColor_UsesTheDefaultForeColor ()
{
// HighlightEntry.ForegroundColor defaults to Color.Empty ("not set"). Both callers of
// a highlight without a color must end up with exactly the same result.
Assert.Multiple(() =>
{
Assert.That(PaintHelper.GetForeColorFromHighlightEntry(null, darkMode: true),
Is.EqualTo(PaintHelper.GetDefaultForeColor(darkMode: true)));
Assert.That(PaintHelper.GetForeColorFromHighlightEntry(new HighlightEntry(), darkMode: false),
Is.EqualTo(PaintHelper.GetDefaultForeColor(darkMode: false)));
});
}

[Test]
public void GetBookmarkMarkerTextColor_LightMode_KeepsDarkOrange ()
{
var markerColor = PaintHelper.GetBookmarkMarkerTextColor(darkMode: false);

Assert.That(markerColor.ToArgb(), Is.EqualTo(Color.FromArgb(255, 190, 100, 0).ToArgb()));
}

[Test]
public void GetBookmarkMarkerTextColor_DarkMode_IsBrighterAndStaysOrange ()
{
var dark = PaintHelper.GetBookmarkMarkerTextColor(darkMode: true);
var light = PaintHelper.GetBookmarkMarkerTextColor(darkMode: false);

Assert.Multiple(() =>
{
Assert.That(dark.GetBrightness(), Is.GreaterThan(light.GetBrightness()),
$"expected a brighter dark-mode marker but got {dark} vs {light}");
Assert.That(dark.R, Is.GreaterThan(dark.G), "marker is expected to stay orange");
Assert.That(dark.G, Is.GreaterThan(dark.B), "marker is expected to stay orange");
});
}

[Test]
public void GetForeColorFromHighlightEntry_NoEntryInLightMode_FallsBackToBlack ()
{
Expand Down
21 changes: 16 additions & 5 deletions src/LogExpert.UI/Controls/ColorComboBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Drawing.Drawing2D;
using System.Runtime.Versioning;

using LogExpert.UI.Entities;

namespace LogExpert.UI.Controls;

[SupportedOSPlatform("windows")]
Expand Down Expand Up @@ -67,23 +69,32 @@ private void OnColorComboBoxDrawItem (object sender, DrawItemEventArgs e)
e.DrawBackground();
if (e.Index >= 0)
{
var darkMode = Application.IsDarkModeEnabled;

// Same off-white the grid uses, so the swatch labels match the log view.
var textColor = PaintHelper.GetDefaultForeColor(darkMode);
using var textBrush = new SolidBrush(textColor);
using var borderPen = new Pen(textColor);

Rectangle rectangle = new(4, e.Bounds.Top + 2, 30, e.Bounds.Height - 4);
var rectColor = (Color)Items[e.Index];
e.Graphics.FillRectangle(new SolidBrush(rectColor), rectangle);
e.Graphics.DrawRectangle(Pens.Black, rectangle);
using var swatchBrush = new SolidBrush(rectColor);
e.Graphics.FillRectangle(swatchBrush, rectangle);
e.Graphics.DrawRectangle(borderPen, rectangle);

if (e.Index == 0)
{
e.Graphics.DrawString(Resources.ColorComboBox_UI_ColorComboBox_Text_Custom, e.Font, Brushes.Black, new PointF(42, e.Bounds.Top + 2));
e.Graphics.DrawString(Resources.ColorComboBox_UI_ColorComboBox_Text_Custom, e.Font, textBrush, new PointF(42, e.Bounds.Top + 2));
}
else
{
e.Graphics.DrawString(((Color)Items[e.Index]).Name, e.Font, Brushes.Black, new PointF(42, e.Bounds.Top + 2));
e.Graphics.DrawString(((Color)Items[e.Index]).Name, e.Font, textBrush, new PointF(42, e.Bounds.Top + 2));
}

if (!Enabled)
{
HatchBrush brush = new(HatchStyle.Percent50, Color.LightGray, Color.FromArgb(10, Color.LightGray));
var hatchLight = darkMode ? Color.DarkGray : Color.LightGray;
HatchBrush brush = new(HatchStyle.Percent50, hatchLight, Color.FromArgb(10, hatchLight));
rectangle.Inflate(1, 1);
e.Graphics.FillRectangle(brush, rectangle);
brush.Dispose();
Expand Down
23 changes: 19 additions & 4 deletions src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ internal partial class LogWindow : DockContent, ILogPaintContextUI, ILogView, IL
private const string FONT_VERDANA = "Verdana";
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();

// Bookmark marker colors: the dark variants are more saturated so the small square keeps
// enough contrast against the dark grid.
private static readonly Color _bookmarkColor = Color.FromArgb(165, 200, 225);
private static readonly Color _autoBookmarkColor = Color.FromArgb(180, 210, 180);
private static readonly Color _bookmarkColorDark = Color.FromArgb(140, 195, 240);
private static readonly Color _autoBookmarkColorDark = Color.FromArgb(150, 215, 150);

private readonly Image _advancedButtonImage;

private readonly BookmarkDataProvider _bookmarkProvider = new();
Expand Down Expand Up @@ -352,14 +359,15 @@ public LogWindow (ILogWindowCoordinator logWindowCoordinator, string fileName, b
#region Properties

[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public Color BookmarkColor { get; set; } = Color.FromArgb(165, 200, 225);
public Color BookmarkColor { get; set; } = _bookmarkColor;

/// <summary>
/// Color used to paint the bookmark marker in column 0 for auto-generated (highlight-triggered) bookmarks. Uses a
/// lighter/more desaturated shade to distinguish from manual bookmarks.
/// slightly different shade to distinguish from manual bookmarks; the dark variant is brighter, not dimmer,
/// because the marker has to stand out against the dark grid.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public Color AutoBookmarkColor { get; set; } = Color.FromArgb(180, 210, 180);
public Color AutoBookmarkColor { get; set; } = _autoBookmarkColor;

public ILogLineMemoryColumnizer CurrentColumnizer
{
Expand Down Expand Up @@ -2741,6 +2749,13 @@ private void CreateDefaultViewStyle ()
filterGridView.RowsDefaultCellStyle = PaintHelper.GetDataGridDefaultRowStyle(darkMode);
PaintHelper.ApplyGridViewTheme(dataGridView, darkMode);
PaintHelper.ApplyGridViewTheme(filterGridView, darkMode);

// Bookmark marker colors need more saturation against dark backgrounds. Both variants
// are assigned explicitly: the color mode is fixed for the lifetime of the process
// (Program.SetColorMode runs before the first window is created), so this never has to
// be re-applied at runtime.
BookmarkColor = darkMode ? _bookmarkColorDark : _bookmarkColor;
AutoBookmarkColor = darkMode ? _autoBookmarkColorDark : _autoBookmarkColor;
}

[SupportedOSPlatform("windows")]
Expand Down Expand Up @@ -6635,7 +6650,7 @@ public void CellPainting (bool focused, int rowIndex, int columnIndex, bool isFi
var fontName = isFilteredGridView ? FONT_VERDANA : FONT_COURIER_NEW;
var stringToDraw = isFilteredGridView ? "!" : "i";

using var brush2 = new SolidBrush(Color.FromArgb(255, 190, 100, 0)); //dark orange
using var brush2 = new SolidBrush(PaintHelper.GetBookmarkMarkerTextColor(Application.IsDarkModeEnabled));
using var font = new Font(fontName, Preferences.Font.Size, FontStyle.Bold);
e.Graphics.DrawString(stringToDraw, font, brush2, new RectangleF(rect.Left, rect.Top, rect.Width, rect.Height), _format);
}
Expand Down
12 changes: 9 additions & 3 deletions src/LogExpert.UI/Dialogs/Highlight/HighlightEntryDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using LogExpert.Core.Helpers;
using LogExpert.Dialogs;
using LogExpert.UI.Controls;
using LogExpert.UI.Entities;

namespace LogExpert.UI.Dialogs.Highlight;

Expand Down Expand Up @@ -404,11 +405,16 @@ private void UpdatePreview ()
? Resources.HighlightEntryEditDialog_UI_Preview_Placeholder
: text;

_previewLabel.ForeColor = GetSelectedColor(_colorBoxForeground, Color.Black);
// Preview with the same defaults the grid paints with. SystemColors.Window stays white
// in dark mode, which would put the off-white preview text on a white background.
var darkMode = Application.IsDarkModeEnabled;
var defaultBackColor = PaintHelper.GetDefaultBackColor(darkMode);

_previewLabel.ForeColor = GetSelectedColor(_colorBoxForeground, PaintHelper.GetDefaultForeColor(darkMode));

_previewLabel.BackColor = _checkBoxNoBackground != null && _checkBoxNoBackground.Checked
? SystemColors.Window
: GetSelectedColor(_colorBoxBackground, SystemColors.Window);
? defaultBackColor
: GetSelectedColor(_colorBoxBackground, defaultBackColor);

var bold = _checkBoxBold != null && _checkBoxBold.Checked;
var baseFont = Font;
Expand Down
27 changes: 27 additions & 0 deletions src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using LogExpert.UI.Services.TabControllerService;
using LogExpert.UI.Services.ToolLaunchService;
using LogExpert.UI.Services.ToolWindowCoordinatorService;
using LogExpert.UI.Theming;

using NLog;

Expand Down Expand Up @@ -93,6 +94,7 @@ public LogTabWindow (string[] fileNames, int instanceNumber, bool showInstanceNu
InitializeComponent();

ConfigureDockPanel();
ConfigureToolStripDark();

_tabController = new TabController(dockPanel);
InitializeTabControllerEvents();
Expand Down Expand Up @@ -384,6 +386,31 @@ private void ConfigureDockPanel ()
dockPanel.DocumentStyle = DocumentStyle.DockingWindow;
}

/// <summary>
/// Switches this window's ToolStrip family to the dark renderer. WinForms ignores
/// Application.SetColorMode(Dark) for ToolStrips, so the renderer is installed
/// process-wide by <see cref="ToolStripTheme"/>; see there for the global scope.
///
/// ToolStripContainer forces RenderMode = System on the strips it hosts, which overrides
/// the manager renderer. Putting the strips and their panels back to ManagerRenderMode is
/// what makes the container stop fighting us.
/// </summary>
private void ConfigureToolStripDark ()
{
if (!Application.IsDarkModeEnabled)
{
return;
}

ToolStripTheme.Apply();

buttonToolStrip.RenderMode = ToolStripRenderMode.ManagerRenderMode;
externalToolsToolStrip.RenderMode = ToolStripRenderMode.ManagerRenderMode;
mainMenuStrip.RenderMode = ToolStripRenderMode.ManagerRenderMode;
statusStrip.RenderMode = ToolStripRenderMode.ManagerRenderMode;
toolStripContainer.TopToolStripPanel.RenderMode = ToolStripRenderMode.ManagerRenderMode;
}

private void ApplyTextResources ()
{
mainMenuStrip.Text = Resources.LogTabWindow_UI_MenuStrip_MainMenu;
Expand Down
Loading
Loading