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
10 changes: 9 additions & 1 deletion src/LogExpert.Core/Config/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ namespace LogExpert.Core.Config;
[Serializable]
public class Preferences
{
private SelectionHighlightSettings _selectionHighlight = new();

public SelectionHighlightSettings SelectionHighlight
{
get => _selectionHighlight;
set => _selectionHighlight = value ?? new();
}

/// <summary>
/// List of highlight groups for syntax highlighting and text coloring.
/// </summary>
Expand Down Expand Up @@ -236,4 +244,4 @@ public Font Font
public float FontSize { get => field; set => field = MathF.Round(value, 1); } = 9.0f;

public List<HighlightMaskEntry> HighlightMaskList { get; set; } = [];
}
}
13 changes: 13 additions & 0 deletions src/LogExpert.Core/Config/SelectionHighlightSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Drawing;

namespace LogExpert.Core.Config;

/// <summary>Application-wide selection appearance, edited in the Highlights dialog.</summary>
[Serializable]
public sealed class SelectionHighlightSettings
{
public bool Outline { get; set; }

/// <summary>Null follows the system selection color.</summary>
public Color? CustomColor { get; set; }
}
15 changes: 15 additions & 0 deletions src/LogExpert.Resources/Resources.Designer.cs

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

14 changes: 13 additions & 1 deletion src/LogExpert.Resources/Resources.de.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2231,4 +2231,16 @@ LogExpert neu starten, um die Änderungen zu übernehmen?</value>
<data name="LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured" xml:space="preserve">
<value>Sysout für das Tool wurde konfiguriert, aber es gibt keine aktive Logdatei. Das Tool wird ohne die Sysout-Pipe gestartet</value>
</data>
</root>
<data name="HighlightDialog_UI_SelectionHighlight" xml:space="preserve">
<value>Auswahlhervorhebung (alle Gruppen)</value>
</data>
<data name="HighlightDialog_UI_OutlineSelectedBlocks" xml:space="preserve">
<value>Ausgewählte Blöcke umranden</value>
</data>
<data name="HighlightDialog_UI_ChangeSelectionColor" xml:space="preserve">
<value>Farbe der Auswahlhervorhebung ändern...</value>
</data>
<data name="HighlightDialog_UI_ResetSelectionColor" xml:space="preserve">
<value>Systemfarbe verwenden</value>
</data>
</root>
14 changes: 13 additions & 1 deletion src/LogExpert.Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2265,4 +2265,16 @@ Restart LogExpert to apply changes?</value>
<data name="LogTabWindow_UI_Message_NoLogfileWithSysOutPipeToolConfigured" xml:space="preserve">
<value>Sysout for the Tool was configured, but there is no active logfile the Tool will be started without the Sysout Pipe</value>
</data>
</root>
<data name="HighlightDialog_UI_SelectionHighlight" xml:space="preserve">
<value>Selection highlight (all groups)</value>
</data>
<data name="HighlightDialog_UI_OutlineSelectedBlocks" xml:space="preserve">
<value>Outline selected blocks</value>
</data>
<data name="HighlightDialog_UI_ChangeSelectionColor" xml:space="preserve">
<value>Change selection highlight color...</value>
</data>
<data name="HighlightDialog_UI_ResetSelectionColor" xml:space="preserve">
<value>Use system color</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using LogExpert.Core.Config;

using Newtonsoft.Json;

using NUnit.Framework;

namespace LogExpert.Tests.ConfigManagerTests;

[TestFixture]
public class SelectionHighlightSettingsTests
{
[Test]
public void CustomAppearance_SurvivesRoundTrip ()
{
var preferences = new Preferences
{
SelectionHighlight = new() { Outline = true, CustomColor = Color.FromArgb(25, 100, 180) }
};

var restored = JsonConvert.DeserializeObject<Preferences>(JsonConvert.SerializeObject(preferences));

Assert.That(restored.SelectionHighlight.Outline, Is.True);
Assert.That(restored.SelectionHighlight.CustomColor, Is.EqualTo(Color.FromArgb(25, 100, 180)));
}

[Test]
public void OlderSettings_KeepSystemFilledSelection ()
{
var preferences = JsonConvert.DeserializeObject<Preferences>("{}");

Assert.That(preferences.SelectionHighlight.Outline, Is.False);
Assert.That(preferences.SelectionHighlight.CustomColor, Is.Null);
}
}
88 changes: 88 additions & 0 deletions src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using LogExpert.Core.Config;
using LogExpert.Core.Interfaces;
using LogExpert.Dialogs;

using Moq;

using NUnit.Framework;

namespace LogExpert.Tests.UI;

[TestFixture]
[Apartment(ApartmentState.STA)]
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public class SelectionHighlightDialogTests
{
[TestCase("en-US", 1f)]
[TestCase("de-DE", 1.5f)]
public void SelectionControls_FitDialogWithoutAnOpenFile (string culture, float scale)
{
var previousCulture = Thread.CurrentThread.CurrentUICulture;
try
{
Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(culture);

var settings = new Settings();
settings.Preferences.SelectionHighlight.CustomColor = Color.Yellow;

var config = new Mock<IConfigManager>();
_ = config.SetupGet(c => c.Settings).Returns(settings);

using var dialog = new HighlightDialog(config.Object) { HighlightGroupList = [] };
dialog.Scale(new SizeF(scale, scale));
dialog.Show();

foreach (var name in new[] { "checkBoxSelectionOutline", "btnSelectionColor", "btnResetSelectionColor", "btnOk", "btnCancel" })
{
var control = dialog.Controls.Find(name, true).Single();

Assert.That(control.Visible, Is.True, name);
Assert.That(control.Parent.ClientRectangle.Contains(control.Bounds), Is.True, name);
Assert.That(dialog.RectangleToScreen(dialog.ClientRectangle).Contains(control.RectangleToScreen(control.ClientRectangle)), Is.True, name);
}

using var bitmap = new Bitmap(dialog.Width, dialog.Height);
dialog.DrawToBitmap(bitmap, new Rectangle(Point.Empty, bitmap.Size));

var path = Path.Join(TestContext.CurrentContext.WorkDirectory, $"selection-highlight-{culture}.png");
bitmap.Save(path, System.Drawing.Imaging.ImageFormat.Png);

TestContext.AddTestAttachment(path);
((Button)dialog.Controls.Find("btnResetSelectionColor", true).Single()).PerformClick();

Assert.That(dialog.SelectionHighlight.CustomColor, Is.Null);
Assert.That(settings.Preferences.SelectionHighlight.CustomColor, Is.EqualTo(Color.Yellow));
}
finally
{
Thread.CurrentThread.CurrentUICulture = previousCulture;
}
}

[TestCase(DialogResult.OK)]
[TestCase(DialogResult.Cancel)]
public void EditingSelection_LeavesLiveSettingsUntouchedUntilCallerAccepts (DialogResult result)
{
var settings = new Settings();

var config = new Mock<IConfigManager>();
_ = config.SetupGet(c => c.Settings).Returns(settings);

using var dialog = new HighlightDialog(config.Object) { HighlightGroupList = [] };
dialog.Show();

var outline = (CheckBox)dialog.Controls.Find("checkBoxSelectionOutline", true).Single();
outline.Checked = true;
((Button)dialog.Controls.Find(result == DialogResult.OK ? "btnOk" : "btnCancel", true).Single()).PerformClick();

Assert.That(dialog.DialogResult, Is.EqualTo(result));
Assert.That(settings.Preferences.SelectionHighlight.Outline, Is.False);

if (result == DialogResult.OK)
{
Assert.That(dialog.SelectionHighlight.Outline, Is.True);
}

config.Verify(c => c.Save(It.IsAny<SettingsFlags>()), Times.Never);
}
}
173 changes: 173 additions & 0 deletions src/LogExpert.Tests/UI/SelectionPainterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using LogExpert.Core.Config;
using LogExpert.UI.Entities;
using LogExpert.UI.Controls;
using LogExpert.UI.Interface;
using LogExpert.Core.Classes.Highlight;
using LogExpert.Core.Entities;
using ColumnizerLib;

using Moq;

using NUnit.Framework;

namespace LogExpert.Tests.UI;

[TestFixture]
[Apartment(ApartmentState.STA)]
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
public class SelectionPainterTests
{
[TestCase(true)]
[TestCase(false)]
public void LogContentPainting_OutlinePreservesLineAndWordBackgrounds (bool outline)
{
var settings = new SelectionHighlightSettings { Outline = outline, CustomColor = Color.Magenta };
var context = new Mock<ILogPaintContextUI>();
context.SetupGet(c => c.SelectionHighlight).Returns(settings);
context.Setup(c => c.GetLogLineMemory(It.IsAny<int>())).Returns(new LogLine("WORD plain", 0));
context.Setup(c => c.FindHighlightEntry(It.IsAny<ITextValueMemory>(), true))
.Returns(new HighlightEntry { BackgroundColor = Color.LightSalmon, ForegroundColor = Color.Black });
context.Setup(c => c.FindHighlightMatches(It.IsAny<ITextValueMemory>())).Returns(() => new List<HighlightMatchEntry>
{
new() { StartPos = 0, Length = 4, HighlightEntry = new() { IsWordMatch = true, BackgroundColor = Color.Yellow, ForegroundColor = Color.Red } }
});
context.SetupGet(c => c.NormalFont).Returns(() => new Font("Consolas", 10));
context.SetupGet(c => c.BoldFont).Returns(() => new Font("Consolas", 10, FontStyle.Bold));
using var form = new Form { ClientSize = new Size(320, 180) };
using var grid = new BufferedDataGridView
{
Dock = DockStyle.Fill, AllowUserToAddRows = false, RowHeadersVisible = false,
ColumnHeadersVisible = false, CellBorderStyle = DataGridViewCellBorderStyle.None,
SelectionMode = DataGridViewSelectionMode.FullRowSelect, SelectionHighlight = settings
};
grid.Columns.Add(new DataGridViewTextBoxColumn { Width = 280 });
grid.RowCount = 2;
grid.CellValueNeeded += (_, e) => e.Value = new Column { FullValue = "WORD plain".AsMemory() };
grid.CellPainting += (_, e) => PaintHelper.CellPainting(context.Object, grid.Focused, e.RowIndex, e.ColumnIndex, e);
form.Controls.Add(grid);
form.Show();
grid.Rows[0].Selected = true;
using var bitmap = new Bitmap(grid.Width, grid.Height);
grid.DrawToBitmap(bitmap, grid.ClientRectangle);
var bounds = grid.GetCellDisplayRectangle(0, 0, false);
Assert.That(bitmap.GetPixel(bounds.Left + 4, bounds.Top + 3).ToArgb(), Is.EqualTo((outline ? Color.Yellow : Color.Magenta).ToArgb()));
Assert.That(bitmap.GetPixel(bounds.Right - 10, bounds.Top + 3).ToArgb(), Is.EqualTo((outline ? Color.LightSalmon : Color.Magenta).ToArgb()));
}

[Test]
public void PaintedBlock_HasNoInternalEdge_AndErasesOldEdgesAfterSelectionChanges ()
{
using var form = new Form { ClientSize = new Size(320, 220) };
using var grid = new BufferedDataGridView
{
Dock = DockStyle.Fill,
AllowUserToAddRows = false,
RowHeadersVisible = false,
ColumnHeadersVisible = false,
CellBorderStyle = DataGridViewCellBorderStyle.None,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
SelectionHighlight = new() { Outline = true, CustomColor = Color.Magenta }
};
grid.Columns.Add("a", "A");
grid.Columns.Add("b", "B");
grid.Rows.Add(4);
// Supply highlighted content beneath the selection overlay.
grid.DefaultCellStyle.BackColor = Color.Khaki;
grid.DefaultCellStyle.SelectionBackColor = Color.Khaki;
form.Controls.Add(grid);
form.Show();
grid.ClearSelection();
grid.Rows[0].Selected = true;
grid.Rows[1].Selected = true;
using var bitmap = new Bitmap(grid.Width, grid.Height);
grid.DrawToBitmap(bitmap, grid.ClientRectangle);
var first = grid.GetCellDisplayRectangle(0, 0, false);
var second = grid.GetCellDisplayRectangle(0, 1, false);

Assert.That(bitmap.GetPixel(first.Left + 20, first.Top).ToArgb(), Is.EqualTo(Color.Magenta.ToArgb()));
Assert.That(bitmap.GetPixel(first.Left + 20, first.Bottom - 1).ToArgb(), Is.EqualTo(Color.Khaki.ToArgb()));
Assert.That(bitmap.GetPixel(second.Left + 20, second.Bottom - 1).ToArgb(), Is.EqualTo(Color.Magenta.ToArgb()));

grid.Rows[1].Selected = false;
grid.DrawToBitmap(bitmap, grid.ClientRectangle);
Assert.That(bitmap.GetPixel(first.Left + 20, first.Bottom - 1).ToArgb(), Is.EqualTo(Color.Magenta.ToArgb()));
Assert.That(bitmap.GetPixel(second.Left + 20, second.Bottom - 1).ToArgb(), Is.EqualTo(Color.Khaki.ToArgb()));
}

[Test]
public void Outline_PreservesBlackAndColoredText ()
{
var style = SelectionPainter.GetStyle(new() { Outline = true, CustomColor = Color.Yellow }, true, true, Color.Blue, false);
Assert.That(style.FillBackground, Is.False);
Assert.That(style.Foreground(Color.Black), Is.EqualTo(Color.Black));
Assert.That(style.Foreground(Color.Red), Is.EqualTo(Color.Red));
}

[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(false, true)]
public void CustomFill_UsesReadableTextEvenWithoutFocus (bool focused, bool darkMode)
{
var style = SelectionPainter.GetStyle(new() { CustomColor = Color.Yellow }, true, focused, Color.Blue, darkMode);
Assert.That(style.Background, Is.EqualTo(Color.Yellow));
Assert.That(style.Foreground(Color.White), Is.EqualTo(Color.Black));
}

[Test]
public void DefaultFill_PreservesLegacyForegroundRules ()
{
var style = SelectionPainter.GetStyle(new(), true, true, Color.Blue, false);
Assert.That(style.Foreground(Color.Black), Is.EqualTo(Color.White));
Assert.That(style.Foreground(Color.Red), Is.EqualTo(Color.Red));
}

[Test]
public void DisjointRows_EachHaveTopAndBottomBoundaries ()
{
using var grid = CreateGrid();
grid.Rows[0].Selected = true;
grid.Rows[2].Selected = true;
Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 0), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Bottom | SelectionEdges.Left));
Assert.That(SelectionPainter.GetOutlineEdges(grid, 2, 0), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Bottom | SelectionEdges.Left));
}

[Test]
public void CellSelection_UsesVisibleDisplayOrder ()
{
using var grid = CreateGrid();
grid.SelectionMode = DataGridViewSelectionMode.CellSelect;
grid.Columns.Add("hidden", "Hidden");
grid.Columns[2].Visible = false;
grid.Columns[1].DisplayIndex = 0;
grid[0, 0].Selected = true;
grid[1, 0].Selected = true;
Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 1), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Bottom | SelectionEdges.Left));
Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 0), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Bottom | SelectionEdges.Right));

grid[1, 0].Selected = false;
Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 0), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Bottom | SelectionEdges.Left | SelectionEdges.Right));
}

[Test]
public void AdjacentRows_HaveOnlyAnExternalOutline ()
{
using var grid = CreateGrid();
grid.Rows[0].Selected = true;
grid.Rows[1].Selected = true;

Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 0), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Left));
Assert.That(SelectionPainter.GetOutlineEdges(grid, 0, 1), Is.EqualTo(SelectionEdges.Top | SelectionEdges.Right));
Assert.That(SelectionPainter.GetOutlineEdges(grid, 1, 0), Is.EqualTo(SelectionEdges.Bottom | SelectionEdges.Left));
Assert.That(SelectionPainter.GetOutlineEdges(grid, 2, 0), Is.EqualTo(SelectionEdges.None));
}

private static DataGridView CreateGrid ()
{
var grid = new DataGridView { AllowUserToAddRows = false, SelectionMode = DataGridViewSelectionMode.FullRowSelect };
grid.Columns.Add("a", "A");
grid.Columns.Add("b", "B");
grid.Rows.Add(4);
grid.ClearSelection();
return grid;
}
}
Loading
Loading