From 8ecab3d56bdb79ea4394562c83eb0b2d20985d68 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Sun, 6 Sep 2026 17:18:09 +0200 Subject: [PATCH 1/2] Add configurable selection color and block outlines (#14) --- src/LogExpert.Core/Config/Preferences.cs | 10 +- .../Config/SelectionHighlightSettings.cs | 13 ++ src/LogExpert.Resources/Resources.Designer.cs | 15 ++ src/LogExpert.Resources/Resources.de.resx | 14 +- src/LogExpert.Resources/Resources.resx | 14 +- .../SelectionHighlightSettingsTests.cs | 34 ++++ .../UI/SelectionHighlightDialogTests.cs | 75 ++++++++ .../UI/SelectionPainterTests.cs | 173 ++++++++++++++++++ .../Controls/BufferedDataGridView.cs | 38 ++++ .../Controls/LogWindow/LogWindow.cs | 37 ++-- src/LogExpert.UI/Dialogs/BookmarkWindow.cs | 8 +- .../Highlight/HighlightDialog.Designer.cs | 51 +++++- .../Dialogs/Highlight/HighlightDialog.cs | 39 ++++ .../Dialogs/LogTabWindow/LogTabWindow.cs | 3 +- src/LogExpert.UI/Entities/PaintHelper.cs | 40 ++-- src/LogExpert.UI/Entities/SelectionPainter.cs | 108 +++++++++++ .../Interface/ILogPaintContextUI.cs | 5 +- 17 files changed, 616 insertions(+), 61 deletions(-) create mode 100644 src/LogExpert.Core/Config/SelectionHighlightSettings.cs create mode 100644 src/LogExpert.Tests/ConfigManagerTests/SelectionHighlightSettingsTests.cs create mode 100644 src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs create mode 100644 src/LogExpert.Tests/UI/SelectionPainterTests.cs create mode 100644 src/LogExpert.UI/Entities/SelectionPainter.cs diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs index ff4dd1bdc..8e70cc8a5 100644 --- a/src/LogExpert.Core/Config/Preferences.cs +++ b/src/LogExpert.Core/Config/Preferences.cs @@ -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(); + } + /// /// List of highlight groups for syntax highlighting and text coloring. /// @@ -236,4 +244,4 @@ public Font Font public float FontSize { get => field; set => field = MathF.Round(value, 1); } = 9.0f; public List HighlightMaskList { get; set; } = []; -} \ No newline at end of file +} diff --git a/src/LogExpert.Core/Config/SelectionHighlightSettings.cs b/src/LogExpert.Core/Config/SelectionHighlightSettings.cs new file mode 100644 index 000000000..0f86cae74 --- /dev/null +++ b/src/LogExpert.Core/Config/SelectionHighlightSettings.cs @@ -0,0 +1,13 @@ +using System.Drawing; + +namespace LogExpert.Core.Config; + +/// Application-wide selection appearance, edited in the Highlights dialog. +[Serializable] +public sealed class SelectionHighlightSettings +{ + public bool Outline { get; set; } + + /// Null follows the system selection color. + public Color? CustomColor { get; set; } +} \ No newline at end of file diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs index 222183428..8ddf47ddd 100644 --- a/src/LogExpert.Resources/Resources.Designer.cs +++ b/src/LogExpert.Resources/Resources.Designer.cs @@ -23,6 +23,21 @@ namespace LogExpert { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { + public static string HighlightDialog_UI_SelectionHighlight { + get { return ResourceManager.GetString("HighlightDialog_UI_SelectionHighlight", resourceCulture); } + } + + public static string HighlightDialog_UI_OutlineSelectedBlocks { + get { return ResourceManager.GetString("HighlightDialog_UI_OutlineSelectedBlocks", resourceCulture); } + } + + public static string HighlightDialog_UI_ChangeSelectionColor { + get { return ResourceManager.GetString("HighlightDialog_UI_ChangeSelectionColor", resourceCulture); } + } + + public static string HighlightDialog_UI_ResetSelectionColor { + get { return ResourceManager.GetString("HighlightDialog_UI_ResetSelectionColor", resourceCulture); } + } private static global::System.Resources.ResourceManager resourceMan; diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx index a83dac05c..c4707f6e5 100644 --- a/src/LogExpert.Resources/Resources.de.resx +++ b/src/LogExpert.Resources/Resources.de.resx @@ -2231,4 +2231,16 @@ LogExpert neu starten, um die Änderungen zu übernehmen? Sysout für das Tool wurde konfiguriert, aber es gibt keine aktive Logdatei. Das Tool wird ohne die Sysout-Pipe gestartet - \ No newline at end of file + + Auswahlhervorhebung (alle Gruppen) + + + Ausgewählte Blöcke umranden + + + Farbe der Auswahlhervorhebung ändern... + + + Systemfarbe verwenden + + diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx index 22abd07a2..3b0349b2f 100644 --- a/src/LogExpert.Resources/Resources.resx +++ b/src/LogExpert.Resources/Resources.resx @@ -2265,4 +2265,16 @@ Restart LogExpert to apply changes? Sysout for the Tool was configured, but there is no active logfile the Tool will be started without the Sysout Pipe - \ No newline at end of file + + Selection highlight (all groups) + + + Outline selected blocks + + + Change selection highlight color... + + + Use system color + + diff --git a/src/LogExpert.Tests/ConfigManagerTests/SelectionHighlightSettingsTests.cs b/src/LogExpert.Tests/ConfigManagerTests/SelectionHighlightSettingsTests.cs new file mode 100644 index 000000000..4b4b0bf44 --- /dev/null +++ b/src/LogExpert.Tests/ConfigManagerTests/SelectionHighlightSettingsTests.cs @@ -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(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("{}"); + + Assert.That(preferences.SelectionHighlight.Outline, Is.False); + Assert.That(preferences.SelectionHighlight.CustomColor, Is.Null); + } +} \ No newline at end of file diff --git a/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs b/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs new file mode 100644 index 000000000..842204fc9 --- /dev/null +++ b/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs @@ -0,0 +1,75 @@ +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(); + 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.Combine(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(); + 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()), Times.Never); + } +} \ No newline at end of file diff --git a/src/LogExpert.Tests/UI/SelectionPainterTests.cs b/src/LogExpert.Tests/UI/SelectionPainterTests.cs new file mode 100644 index 000000000..5b12437e4 --- /dev/null +++ b/src/LogExpert.Tests/UI/SelectionPainterTests.cs @@ -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(); + context.SetupGet(c => c.SelectionHighlight).Returns(settings); + context.Setup(c => c.GetLogLineMemory(It.IsAny())).Returns(new LogLine("WORD plain", 0)); + context.Setup(c => c.FindHighlightEntry(It.IsAny(), true)) + .Returns(new HighlightEntry { BackgroundColor = Color.LightSalmon, ForegroundColor = Color.Black }); + context.Setup(c => c.FindHighlightMatches(It.IsAny())).Returns(() => new List + { + 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; + } +} \ No newline at end of file diff --git a/src/LogExpert.UI/Controls/BufferedDataGridView.cs b/src/LogExpert.UI/Controls/BufferedDataGridView.cs index 5ef1f004c..6dccc1677 100644 --- a/src/LogExpert.UI/Controls/BufferedDataGridView.cs +++ b/src/LogExpert.UI/Controls/BufferedDataGridView.cs @@ -3,7 +3,9 @@ using System.Runtime.Versioning; using LogExpert.Core.Entities; +using LogExpert.Core.Config; using LogExpert.Core.EventArguments; +using LogExpert.UI.Entities; using NLog; @@ -75,6 +77,9 @@ public BufferedDataGridView () [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] public bool PaintWithOverlays { get; set; } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SelectionHighlightSettings SelectionHighlight { get; set; } = new(); + #endregion #region Public methods @@ -134,6 +139,37 @@ private void EnsureDrawingResources () #region Overrides + protected override void OnSelectionChanged (EventArgs e) + { + base.OnSelectionChanged(e); + if (SelectionHighlight?.Outline == true) + { + // A changed cell can also remove an edge on a still-selected neighbor. + Invalidate(); + } + } + + protected override void OnGotFocus (EventArgs e) + { + base.OnGotFocus(e); + Invalidate(); + } + + protected override void OnScroll (ScrollEventArgs e) + { + base.OnScroll(e); + if (SelectionHighlight?.Outline == true) + { + Invalidate(); + } + } + + protected override void OnLostFocus (EventArgs e) + { + base.OnLostFocus(e); + Invalidate(); + } + protected override void Dispose (bool disposing) { if (disposing) @@ -161,6 +197,8 @@ protected override void OnPaint (PaintEventArgs e) { base.OnPaint(e); } + + SelectionPainter.PaintOutline(this, e, SelectionHighlight); } catch (Exception ex) { diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs index c5c23c35d..38e11b04d 100644 --- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs +++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs @@ -421,6 +421,8 @@ public bool IsMultiFile public Preferences Preferences => ConfigManager.Settings.Preferences; + SelectionHighlightSettings ILogPaintContextUI.SelectionHighlight => Preferences.SelectionHighlight; + [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] public string GivenFileName { get; set; } @@ -2237,6 +2239,8 @@ private void OnSetBookmarksOnSelectedLinesToolStripMenuItemClick (object sender, private void OnParentHighlightSettingsChanged (object sender, EventArgs e) { + dataGridView.SelectionHighlight = Preferences.SelectionHighlight; + filterGridView.SelectionHighlight = Preferences.SelectionHighlight; var groupName = _guiStateArgs.HighlightGroupName; SetCurrentHighlightGroup(groupName); } @@ -3514,12 +3518,12 @@ private void AdjustColumnWidthsForControlCharSubstitution (BufferedDataGridView } } - private void PaintCell (DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry) + private void PaintCell (DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry, SelectionCellStyle selection) { - PaintHighlightedCell(e, groundEntry); + PaintHighlightedCell(e, groundEntry, selection); } - private void PaintHighlightedCell (DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry) + private void PaintHighlightedCell (DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry, SelectionCellStyle selection) { var column = e.Value as IColumnMemory; @@ -3618,15 +3622,8 @@ private void PaintHighlightedCell (DataGridViewCellPaintingEventArgs e, Highligh wordSize.Height = e.CellBounds.Height; Rectangle wordRect = new(wordPos, wordSize); - var foreColor = segment.ForeColor; - if (e.State.HasFlag(DataGridViewElementStates.Selected)) - { - if (foreColor.Equals(Color.Black)) - { - foreColor = Color.White; - } - } - else + var foreColor = selection.Foreground(segment.ForeColor); + if (!selection.FillBackground) { if (bgBrush != null && !segment.NoBackground) { @@ -6256,12 +6253,9 @@ public void CellPainting (bool focused, int rowIndex, int columnIndex, bool isFi var entry = FindFirstNoWordMatchHighlightEntry(line); e.Graphics.SetClip(e.CellBounds); - if (e.State.HasFlag(DataGridViewElementStates.Selected)) - { - using var brush = PaintHelper.GetBrushForFocusedControl(focused, e.CellStyle.SelectionBackColor, Application.IsDarkModeEnabled); - e.Graphics.FillRectangle(brush, e.CellBounds); - } - else + var selection = SelectionPainter.GetStyle(Preferences.SelectionHighlight, + e.State.HasFlag(DataGridViewElementStates.Selected), focused, e.CellStyle.SelectionBackColor, Application.IsDarkModeEnabled); + if (!selection.PaintBackground(e)) { e.CellStyle.BackColor = PaintHelper.GetBackColorFromHighlightEntry(entry, Application.IsDarkModeEnabled); e.PaintBackground(e.ClipBounds, false); @@ -6269,11 +6263,12 @@ public void CellPainting (bool focused, int rowIndex, int columnIndex, bool isFi if (DebugOptions.DisableWordHighlight) { + e.CellStyle.SelectionForeColor = selection.Foreground(e.CellStyle.ForeColor); e.PaintContent(e.CellBounds); } else { - PaintCell(e, entry); + PaintCell(e, entry, selection); } if (columnIndex == 0) @@ -7346,6 +7341,8 @@ public void Reload () public void PreferencesChanged (Font font, bool setLastColumnWidth, int lastColumnWidth, bool isLoadTime, SettingsFlags flags) { + dataGridView.SelectionHighlight = Preferences.SelectionHighlight; + filterGridView.SelectionHighlight = Preferences.SelectionHighlight; if ((flags & SettingsFlags.GuiOrColors) == SettingsFlags.GuiOrColors) { font ??= Preferences.Font ?? new Font(FontFamily.GenericMonospace, 9f); @@ -7733,4 +7730,4 @@ public void RefreshLogView () } #endregion -} \ No newline at end of file +} diff --git a/src/LogExpert.UI/Dialogs/BookmarkWindow.cs b/src/LogExpert.UI/Dialogs/BookmarkWindow.cs index 2dd0c2881..104e25ddc 100644 --- a/src/LogExpert.UI/Dialogs/BookmarkWindow.cs +++ b/src/LogExpert.UI/Dialogs/BookmarkWindow.cs @@ -197,6 +197,11 @@ public void SetBookmarkData (IBookmarkData bookmarkData) public void PreferencesChanged (Font font, bool setLastColumnWidth, int lastColumnWidth, SettingsFlags flags) { + if (_logPaintContext != null) + { + bookmarkDataGridView.SelectionHighlight = _logPaintContext.SelectionHighlight; + bookmarkDataGridView.Invalidate(); + } if ((flags & SettingsFlags.GuiOrColors) == SettingsFlags.GuiOrColors) { SetFont(font); @@ -218,6 +223,7 @@ public void SetCurrentFile (IFileViewContext ctx) { _logView = ctx.LogView; _logPaintContext = (ILogPaintContextUI)ctx.LogPaintContext; + bookmarkDataGridView.SelectionHighlight = _logPaintContext.SelectionHighlight; } SetColumnizer(ctx.LogView.CurrentColumnizer); @@ -671,4 +677,4 @@ private void OnBookmarkWindowSizeChanged (object sender, EventArgs e) } #endregion -} \ No newline at end of file +} diff --git a/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.Designer.cs b/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.Designer.cs index bc99f4a94..e17c4eb3f 100644 --- a/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.Designer.cs +++ b/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.Designer.cs @@ -53,6 +53,11 @@ private void InitializeComponent () toolTip = new ToolTip(components); pnlBackground = new Panel(); groupBoxGroups = new GroupBox(); + groupBoxSelection = new GroupBox(); + checkBoxSelectionOutline = new CheckBox(); + btnSelectionColor = new Button(); + btnResetSelectionColor = new Button(); + selectionControls = new FlowLayoutPanel(); pnlBackground.SuspendLayout(); groupBoxGroups.SuspendLayout(); SuspendLayout(); @@ -139,7 +144,7 @@ private void InitializeComponent () // btnOk.Anchor = AnchorStyles.Top; btnOk.DialogResult = DialogResult.OK; - btnOk.Location = new Point(387, 364); + btnOk.Location = new Point(387, 480); btnOk.Margin = new Padding(4, 5, 4, 5); btnOk.Name = "btnOk"; btnOk.Size = new Size(85, 35); @@ -152,7 +157,7 @@ private void InitializeComponent () // btnCancel.Anchor = AnchorStyles.Top; btnCancel.DialogResult = DialogResult.Cancel; - btnCancel.Location = new Point(478, 364); + btnCancel.Location = new Point(478, 480); btnCancel.Margin = new Padding(4, 5, 4, 5); btnCancel.Name = "btnCancel"; btnCancel.Size = new Size(85, 35); @@ -168,7 +173,7 @@ private void InitializeComponent () // btnExportGroup // btnExportGroup.Anchor = AnchorStyles.Top; - btnExportGroup.Location = new Point(108, 364); + btnExportGroup.Location = new Point(108, 480); btnExportGroup.Margin = new Padding(4, 5, 4, 5); btnExportGroup.Name = "btnExportGroup"; btnExportGroup.Size = new Size(85, 35); @@ -181,7 +186,7 @@ private void InitializeComponent () // btnImportGroup // btnImportGroup.Anchor = AnchorStyles.Top; - btnImportGroup.Location = new Point(12, 364); + btnImportGroup.Location = new Point(12, 480); btnImportGroup.Margin = new Padding(4, 5, 4, 5); btnImportGroup.Name = "btnImportGroup"; btnImportGroup.Size = new Size(85, 35); @@ -297,9 +302,10 @@ private void InitializeComponent () pnlBackground.Controls.Add(btnOk); pnlBackground.Controls.Add(btnCancel); pnlBackground.Controls.Add(groupBoxGroups); + pnlBackground.Controls.Add(groupBoxSelection); pnlBackground.Location = new Point(0, 0); pnlBackground.Name = "pnlBackground"; - pnlBackground.Size = new Size(576, 511); + pnlBackground.Size = new Size(576, 528); pnlBackground.TabIndex = 23; // // groupBoxGroups @@ -320,12 +326,38 @@ private void InitializeComponent () groupBoxGroups.TabIndex = 22; groupBoxGroups.TabStop = false; groupBoxGroups.Text = "Groups"; + // Selection appearance is application-wide, separate from the group editor. + groupBoxSelection.Location = new Point(12, 364); + groupBoxSelection.Size = new Size(552, 108); + groupBoxSelection.TabIndex = 8; + groupBoxSelection.Controls.Add(selectionControls); + selectionControls.Dock = DockStyle.Fill; + selectionControls.Padding = new Padding(6); + selectionControls.Controls.Add(checkBoxSelectionOutline); + selectionControls.Controls.Add(btnSelectionColor); + selectionControls.Controls.Add(btnResetSelectionColor); + selectionControls.SetFlowBreak(checkBoxSelectionOutline, true); + checkBoxSelectionOutline.Name = "checkBoxSelectionOutline"; + checkBoxSelectionOutline.AutoSize = true; + checkBoxSelectionOutline.TabIndex = 0; + btnSelectionColor.AutoSize = true; + btnSelectionColor.Name = "btnSelectionColor"; + btnSelectionColor.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnSelectionColor.Padding = new Padding(4); + btnSelectionColor.TabIndex = 1; + btnSelectionColor.Click += OnSelectionColorClick; + btnResetSelectionColor.AutoSize = true; + btnResetSelectionColor.Name = "btnResetSelectionColor"; + btnResetSelectionColor.AutoSizeMode = AutoSizeMode.GrowAndShrink; + btnResetSelectionColor.Padding = new Padding(4); + btnResetSelectionColor.TabIndex = 2; + btnResetSelectionColor.Click += OnResetSelectionColorClick; // // HighlightDialog // AcceptButton = btnOk; CancelButton = btnCancel; - ClientSize = new Size(576, 411); + ClientSize = new Size(576, 528); Controls.Add(pnlBackground); DoubleBuffered = true; helpProvider.SetHelpKeyword(this, "Highlighting.htm"); @@ -335,7 +367,7 @@ private void InitializeComponent () Margin = new Padding(4, 5, 4, 5); MaximizeBox = false; MinimizeBox = false; - MinimumSize = new Size(592, 430); + MinimumSize = new Size(592, 567); Name = "HighlightDialog"; helpProvider.SetShowHelp(this, true); StartPosition = FormStartPosition.CenterParent; @@ -348,6 +380,11 @@ private void InitializeComponent () } #endregion + private GroupBox groupBoxSelection; + private FlowLayoutPanel selectionControls; + private CheckBox checkBoxSelectionOutline; + private Button btnSelectionColor; + private Button btnResetSelectionColor; private System.Windows.Forms.ListBox listBoxHighlight; private System.Windows.Forms.Button btnAdd; diff --git a/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.cs b/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.cs index 89b324de2..89afc7636 100644 --- a/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.cs +++ b/src/LogExpert.UI/Dialogs/Highlight/HighlightDialog.cs @@ -3,6 +3,7 @@ using System.Security; using ColumnizerLib; +using LogExpert.Core.Config; using LogExpert.Core.Classes.Highlight; using LogExpert.Core.Entities; @@ -20,6 +21,7 @@ internal partial class HighlightDialog : Form private HighlightGroup _currentGroup; private List _highlightGroupList; + private Color? _selectionColor; #endregion @@ -37,6 +39,9 @@ public HighlightDialog (IConfigManager configManager) ApplyResources(); ConfigManager = configManager; + _selectionColor = configManager.Settings.Preferences.SelectionHighlight.CustomColor; + checkBoxSelectionOutline.Checked = configManager.Settings.Preferences.SelectionHighlight.Outline; + UpdateSelectionColorPreview(); Load += OnHighlightDialogLoad; listBoxHighlight.DrawItem += OnHighlightListBoxDrawItem; @@ -65,6 +70,10 @@ private void ApplyResources () labelAssignNamesToGroups.Text = Resources.HighlightDialog_UI_Label_AssignNamesToGroups; groupBoxGroups.Text = Resources.HighlightDialog_UI_GroupBox_Groups; + groupBoxSelection.Text = Resources.HighlightDialog_UI_SelectionHighlight; + checkBoxSelectionOutline.Text = Resources.HighlightDialog_UI_OutlineSelectedBlocks; + btnSelectionColor.Text = Resources.HighlightDialog_UI_ChangeSelectionColor; + btnResetSelectionColor.Text = Resources.HighlightDialog_UI_ResetSelectionColor; } #endregion @@ -94,10 +103,40 @@ public List HighlightGroupList private IConfigManager ConfigManager { get; } + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public SelectionHighlightSettings SelectionHighlight => new() + { + Outline = checkBoxSelectionOutline.Checked, + CustomColor = _selectionColor + }; + #endregion #region Event handling Methods + private void OnSelectionColorClick (object sender, EventArgs e) + { + using var dialog = new ColorDialog { Color = _selectionColor ?? SystemColors.Highlight, FullOpen = true }; + if (dialog.ShowDialog(this) == DialogResult.OK) + { + _selectionColor = dialog.Color; + UpdateSelectionColorPreview(); + } + } + + private void OnResetSelectionColorClick (object sender, EventArgs e) + { + _selectionColor = null; + UpdateSelectionColorPreview(); + } + + private void UpdateSelectionColorPreview () + { + btnSelectionColor.BackColor = _selectionColor ?? SystemColors.Highlight; + btnSelectionColor.ForeColor = PaintHelper.GetForeColorBasedOnBackColor(btnSelectionColor.BackColor); + btnResetSelectionColor.Enabled = _selectionColor.HasValue; + } + private void OnAddButtonClick (object sender, EventArgs e) { if (_currentGroup == null) diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs index 2e44ec9e8..9c9647c19 100644 --- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs +++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs @@ -915,7 +915,7 @@ private void RemoveAndDisposeLogWindow (LogWindow.LogWindow logWindow, bool dont [SupportedOSPlatform("windows")] private void ShowHighlightSettingsDialog () { - HighlightDialog dlg = new(ConfigManager) + using HighlightDialog dlg = new(ConfigManager) { KeywordActionList = PluginRegistry.PluginRegistry.Instance.RegisteredKeywordActions, Owner = this, @@ -931,6 +931,7 @@ private void ShowHighlightSettingsDialog () HighlightGroupList = dlg.HighlightGroupList; FillHighlightComboBox(); ConfigManager.Settings.Preferences.HighlightGroupList = HighlightGroupList; + ConfigManager.Settings.Preferences.SelectionHighlight = dlg.SelectionHighlight; ConfigManager.Save(SettingsFlags.HighlightSettings); OnHighlightSettingsChanged(); } diff --git a/src/LogExpert.UI/Entities/PaintHelper.cs b/src/LogExpert.UI/Entities/PaintHelper.cs index a3425ea56..404c28d26 100644 --- a/src/LogExpert.UI/Entities/PaintHelper.cs +++ b/src/LogExpert.UI/Entities/PaintHelper.cs @@ -44,12 +44,9 @@ public static void CellPainting (ILogPaintContextUI logPaintCtx, bool focused, i var entry = logPaintCtx.FindHighlightEntry(line, true); e.Graphics.SetClip(e.CellBounds); - if (e.State.HasFlag(DataGridViewElementStates.Selected)) - { - using var brush = GetBrushForFocusedControl(focused, e.CellStyle.SelectionBackColor, Application.IsDarkModeEnabled); - e.Graphics.FillRectangle(brush, e.CellBounds); - } - else + var selection = SelectionPainter.GetStyle(logPaintCtx.SelectionHighlight, + e.State.HasFlag(DataGridViewElementStates.Selected), focused, e.CellStyle.SelectionBackColor, Application.IsDarkModeEnabled); + if (!selection.PaintBackground(e)) { e.CellStyle.BackColor = GetBackColorFromHighlightEntry(entry, Application.IsDarkModeEnabled); e.PaintBackground(e.ClipBounds, false); @@ -57,11 +54,12 @@ public static void CellPainting (ILogPaintContextUI logPaintCtx, bool focused, i if (DebugOptions.DisableWordHighlight) { + e.CellStyle.SelectionForeColor = selection.Foreground(e.CellStyle.ForeColor); e.PaintContent(e.CellBounds); } else { - PaintCell(logPaintCtx, e, entry); + PaintCell(logPaintCtx, e, entry, selection); } if (e.ColumnIndex == 0) @@ -110,14 +108,7 @@ public static Color GetForeColorFromHighlightEntry (HighlightEntry? entry, bool [SupportedOSPlatform("windows")] public static Brush GetBrushForFocusedControl (bool focused, Color selectionColor, bool darkMode) { - if (focused) - { - return new SolidBrush(selectionColor); - } - - return darkMode - ? new SolidBrush(Color.FromArgb(255, 90, 90, 90)) // dark gray - : new SolidBrush(Color.FromArgb(255, 170, 170, 170)); // light gray + return new SolidBrush(SelectionPainter.GetStyle(new(), true, focused, selectionColor, darkMode).Background); } [SupportedOSPlatform("windows")] @@ -374,13 +365,13 @@ private static int GetBorderSize (DataGridViewAdvancedCellBorderStyle borderStyl #region Private Methods [SupportedOSPlatform("windows")] - private static void PaintCell (ILogPaintContextUI logPaintCtx, DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry) + private static void PaintCell (ILogPaintContextUI logPaintCtx, DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry, SelectionCellStyle selection) { - PaintHighlightedCell(logPaintCtx, e, groundEntry); + PaintHighlightedCell(logPaintCtx, e, groundEntry, selection); } [SupportedOSPlatform("windows")] - private static void PaintHighlightedCell (ILogPaintContextUI logPaintCtx, DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry) + private static void PaintHighlightedCell (ILogPaintContextUI logPaintCtx, DataGridViewCellPaintingEventArgs e, HighlightEntry groundEntry, SelectionCellStyle selection) { var value = e.Value ?? string.Empty; @@ -476,15 +467,8 @@ private static void PaintHighlightedCell (ILogPaintContextUI logPaintCtx, DataGr wordSize.Height = e.CellBounds.Height; Rectangle wordRect = new(wordPos, wordSize); - var foreColor = matchEntry.HighlightEntry.ForegroundColor; - if (e.State.HasFlag(DataGridViewElementStates.Selected)) - { - if (foreColor.Equals(Color.Black)) - { - foreColor = Color.White; - } - } - else + var foreColor = selection.Foreground(matchEntry.HighlightEntry.ForegroundColor); + if (!selection.FillBackground) { if (bgBrush != null && !matchEntry.HighlightEntry.NoBackground) { @@ -573,4 +557,4 @@ private static IList MergeHighlightMatchEntries (IListSelection styling and external boundaries shared by all log grids. +[SupportedOSPlatform("windows")] +internal static class SelectionPainter +{ + public static SelectionCellStyle GetStyle (SelectionHighlightSettings settings, bool selected, bool focused, Color systemColor, bool darkMode) + { + var color = settings.CustomColor ?? (focused ? systemColor : darkMode ? Color.FromArgb(90, 90, 90) : Color.FromArgb(170, 170, 170)); + return new(selected && !settings.Outline, color, settings.CustomColor.HasValue); + } + + public static SelectionEdges GetOutlineEdges (DataGridView grid, int row, int column) + { + if (!grid[column, row].Selected) + { + return SelectionEdges.None; + } + + var previousRow = grid.Rows.GetPreviousRow(row, DataGridViewElementStates.Visible); + var nextRow = grid.Rows.GetNextRow(row, DataGridViewElementStates.Visible); + var previousColumn = grid.Columns.GetPreviousColumn(grid.Columns[column], DataGridViewElementStates.Visible, DataGridViewElementStates.None); + var nextColumn = grid.Columns.GetNextColumn(grid.Columns[column], DataGridViewElementStates.Visible, DataGridViewElementStates.None); + var edges = SelectionEdges.None; + if (previousRow < 0 || !grid[column, previousRow].Selected) { edges |= SelectionEdges.Top; } + if (nextRow < 0 || !grid[column, nextRow].Selected) { edges |= SelectionEdges.Bottom; } + if (previousColumn == null || !grid[previousColumn.Index, row].Selected) { edges |= SelectionEdges.Left; } + if (nextColumn == null || !grid[nextColumn.Index, row].Selected) { edges |= SelectionEdges.Right; } + return edges; + } + + public static void PaintOutline (DataGridView grid, PaintEventArgs e, SelectionHighlightSettings settings) + { + if (!settings.Outline || grid.RowCount == 0 || grid.ColumnCount == 0) + { + return; + } + + var style = GetStyle(settings, true, grid.Focused, grid.DefaultCellStyle.SelectionBackColor, Application.IsDarkModeEnabled); + using var pen = new Pen(style.Background, Math.Max(1, grid.DeviceDpi / 96f)); + var state = e.Graphics.Save(); + try + { + // Cell painting changes the graphics clip. Restore the update region before + // drawing boundaries, and clip each cell to its actual visible portion. + e.Graphics.SetClip(e.ClipRectangle); + for (var row = grid.Rows.GetFirstRow(DataGridViewElementStates.Displayed); row >= 0; + row = grid.Rows.GetNextRow(row, DataGridViewElementStates.Displayed)) + { + foreach (DataGridViewColumn column in grid.Columns) + { + if (!column.Visible) { continue; } + var bounds = grid.GetCellDisplayRectangle(column.Index, row, false); + var visible = grid.GetCellDisplayRectangle(column.Index, row, true); + if (visible.IsEmpty || !visible.IntersectsWith(e.ClipRectangle)) { continue; } + var edges = GetOutlineEdges(grid, row, column.Index); + if (edges == SelectionEdges.None) { continue; } + + e.Graphics.SetClip(Rectangle.Intersect(visible, e.ClipRectangle), CombineMode.Replace); + var inset = pen.Width / 2; + var left = bounds.Left + inset; + var right = bounds.Right - inset; + var top = bounds.Top + inset; + var bottom = bounds.Bottom - inset; + if (edges.HasFlag(SelectionEdges.Top)) { e.Graphics.DrawLine(pen, bounds.Left, top, bounds.Right, top); } + if (edges.HasFlag(SelectionEdges.Bottom)) { e.Graphics.DrawLine(pen, bounds.Left, bottom, bounds.Right, bottom); } + if (edges.HasFlag(SelectionEdges.Left)) { e.Graphics.DrawLine(pen, left, bounds.Top, left, bounds.Bottom); } + if (edges.HasFlag(SelectionEdges.Right)) { e.Graphics.DrawLine(pen, right, bounds.Top, right, bounds.Bottom); } + } + } + } + finally + { + e.Graphics.Restore(state); + } + } +} + +[SupportedOSPlatform("windows")] +internal readonly record struct SelectionCellStyle (bool FillBackground, Color Background, bool CustomColor) +{ + public Color Foreground (Color original) => !FillBackground ? original + : CustomColor ? PaintHelper.GetForeColorBasedOnBackColor(Background) + : original == Color.Black ? Color.White : original; + + public bool PaintBackground (DataGridViewCellPaintingEventArgs e) + { + if (!FillBackground) { return false; } + using var brush = new SolidBrush(Background); + e.Graphics.FillRectangle(brush, e.CellBounds); + return true; + } +} \ No newline at end of file diff --git a/src/LogExpert.UI/Interface/ILogPaintContextUI.cs b/src/LogExpert.UI/Interface/ILogPaintContextUI.cs index 5c3b039b1..3c11641ec 100644 --- a/src/LogExpert.UI/Interface/ILogPaintContextUI.cs +++ b/src/LogExpert.UI/Interface/ILogPaintContextUI.cs @@ -1,4 +1,5 @@ using ColumnizerLib; +using LogExpert.Core.Config; using LogExpert.Core.Classes.Highlight; using LogExpert.Core.Entities; @@ -22,6 +23,8 @@ internal interface ILogPaintContextUI : ILogLineSource Color BookmarkColor { get; } + SelectionHighlightSettings SelectionHighlight { get; } + #endregion #region Public methods @@ -37,4 +40,4 @@ internal interface ILogPaintContextUI : ILogLineSource IList FindHighlightMatches (ITextValueMemory line); #endregion -} \ No newline at end of file +} From 18debd1cec10bad1cfa120b3f4bd19ba2f7502c4 Mon Sep 17 00:00:00 2001 From: Hirogen Date: Sun, 6 Sep 2026 17:43:12 +0200 Subject: [PATCH 2/2] prettyfy --- .../UI/SelectionHighlightDialogTests.cs | 19 ++++- src/LogExpert.UI/Entities/SelectionPainter.cs | 71 +++++++++++++++---- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs b/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs index 842204fc9..2feb61fba 100644 --- a/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs +++ b/src/LogExpert.Tests/UI/SelectionHighlightDialogTests.cs @@ -21,16 +21,21 @@ public void SelectionControls_FitDialogWithoutAnOpenFile (string culture, float try { Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(culture); + var settings = new Settings(); settings.Preferences.SelectionHighlight.CustomColor = Color.Yellow; + var config = new Mock(); - config.SetupGet(c => c.Settings).Returns(settings); + _ = 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); @@ -38,10 +43,13 @@ public void SelectionControls_FitDialogWithoutAnOpenFile (string culture, float using var bitmap = new Bitmap(dialog.Width, dialog.Height); dialog.DrawToBitmap(bitmap, new Rectangle(Point.Empty, bitmap.Size)); - var path = Path.Combine(TestContext.CurrentContext.WorkDirectory, $"selection-highlight-{culture}.png"); + + 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)); } @@ -56,20 +64,25 @@ public void SelectionControls_FitDialogWithoutAnOpenFile (string culture, float public void EditingSelection_LeavesLiveSettingsUntouchedUntilCallerAccepts (DialogResult result) { var settings = new Settings(); + var config = new Mock(); - config.SetupGet(c => c.Settings).Returns(settings); + _ = 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()), Times.Never); } } \ No newline at end of file diff --git a/src/LogExpert.UI/Entities/SelectionPainter.cs b/src/LogExpert.UI/Entities/SelectionPainter.cs index 6a7b915e0..4ed9dca67 100644 --- a/src/LogExpert.UI/Entities/SelectionPainter.cs +++ b/src/LogExpert.UI/Entities/SelectionPainter.cs @@ -37,10 +37,26 @@ public static SelectionEdges GetOutlineEdges (DataGridView grid, int row, int co var previousColumn = grid.Columns.GetPreviousColumn(grid.Columns[column], DataGridViewElementStates.Visible, DataGridViewElementStates.None); var nextColumn = grid.Columns.GetNextColumn(grid.Columns[column], DataGridViewElementStates.Visible, DataGridViewElementStates.None); var edges = SelectionEdges.None; - if (previousRow < 0 || !grid[column, previousRow].Selected) { edges |= SelectionEdges.Top; } - if (nextRow < 0 || !grid[column, nextRow].Selected) { edges |= SelectionEdges.Bottom; } - if (previousColumn == null || !grid[previousColumn.Index, row].Selected) { edges |= SelectionEdges.Left; } - if (nextColumn == null || !grid[nextColumn.Index, row].Selected) { edges |= SelectionEdges.Right; } + if (previousRow < 0 || !grid[column, previousRow].Selected) + { + edges |= SelectionEdges.Top; + } + + if (nextRow < 0 || !grid[column, nextRow].Selected) + { + edges |= SelectionEdges.Bottom; + } + + if (previousColumn == null || !grid[previousColumn.Index, row].Selected) + { + edges |= SelectionEdges.Left; + } + + if (nextColumn == null || !grid[nextColumn.Index, row].Selected) + { + edges |= SelectionEdges.Right; + } + return edges; } @@ -64,23 +80,50 @@ public static void PaintOutline (DataGridView grid, PaintEventArgs e, SelectionH { foreach (DataGridViewColumn column in grid.Columns) { - if (!column.Visible) { continue; } + if (!column.Visible) + { + continue; + } + var bounds = grid.GetCellDisplayRectangle(column.Index, row, false); var visible = grid.GetCellDisplayRectangle(column.Index, row, true); - if (visible.IsEmpty || !visible.IntersectsWith(e.ClipRectangle)) { continue; } + if (visible.IsEmpty || !visible.IntersectsWith(e.ClipRectangle)) + { + continue; + } + var edges = GetOutlineEdges(grid, row, column.Index); - if (edges == SelectionEdges.None) { continue; } + if (edges == SelectionEdges.None) + { + continue; + } e.Graphics.SetClip(Rectangle.Intersect(visible, e.ClipRectangle), CombineMode.Replace); + var inset = pen.Width / 2; var left = bounds.Left + inset; var right = bounds.Right - inset; var top = bounds.Top + inset; var bottom = bounds.Bottom - inset; - if (edges.HasFlag(SelectionEdges.Top)) { e.Graphics.DrawLine(pen, bounds.Left, top, bounds.Right, top); } - if (edges.HasFlag(SelectionEdges.Bottom)) { e.Graphics.DrawLine(pen, bounds.Left, bottom, bounds.Right, bottom); } - if (edges.HasFlag(SelectionEdges.Left)) { e.Graphics.DrawLine(pen, left, bounds.Top, left, bounds.Bottom); } - if (edges.HasFlag(SelectionEdges.Right)) { e.Graphics.DrawLine(pen, right, bounds.Top, right, bounds.Bottom); } + if (edges.HasFlag(SelectionEdges.Top)) + { + e.Graphics.DrawLine(pen, bounds.Left, top, bounds.Right, top); + } + + if (edges.HasFlag(SelectionEdges.Bottom)) + { + e.Graphics.DrawLine(pen, bounds.Left, bottom, bounds.Right, bottom); + } + + if (edges.HasFlag(SelectionEdges.Left)) + { + e.Graphics.DrawLine(pen, left, bounds.Top, left, bounds.Bottom); + } + + if (edges.HasFlag(SelectionEdges.Right)) + { + e.Graphics.DrawLine(pen, right, bounds.Top, right, bounds.Bottom); + } } } } @@ -100,7 +143,11 @@ public Color Foreground (Color original) => !FillBackground ? original public bool PaintBackground (DataGridViewCellPaintingEventArgs e) { - if (!FillBackground) { return false; } + if (!FillBackground) + { + return false; + } + using var brush = new SolidBrush(Background); e.Graphics.FillRectangle(brush, e.CellBounds); return true;