diff --git a/src/LogExpert.Configuration/ConfigManager.cs b/src/LogExpert.Configuration/ConfigManager.cs
index eb1d8440..702ba130 100644
--- a/src/LogExpert.Configuration/ConfigManager.cs
+++ b/src/LogExpert.Configuration/ConfigManager.cs
@@ -328,7 +328,6 @@ public void RemoveFromFileHistory (string fileName)
Save(SettingsFlags.FileHistory);
}
-
public void ClearLastOpenFilesList ()
{
lock (_loadSaveLock)
@@ -596,6 +595,9 @@ UnauthorizedAccessException or
private static Settings InitializeSettings (Settings settings)
{
+ // Apply any pending schema migrations before any consumer reads the settings.
+ _ = LegacyPreferencesMigrator.Migrate(settings);
+
settings.Preferences ??= new Preferences();
settings.Preferences.ToolEntries ??= [];
settings.Preferences.ColumnizerMaskList ??= [];
diff --git a/src/LogExpert.Configuration/LegacyPreferencesMigrator.cs b/src/LogExpert.Configuration/LegacyPreferencesMigrator.cs
new file mode 100644
index 00000000..cb31a16d
--- /dev/null
+++ b/src/LogExpert.Configuration/LegacyPreferencesMigrator.cs
@@ -0,0 +1,58 @@
+using LogExpert.Core.Config;
+
+namespace LogExpert.Configuration;
+
+///
+/// Applies one-shot, in-memory migrations to a freshly deserialised object so that
+/// older settings files behave equivalently under newer schema versions. Idempotent: calling it on already
+/// up-to-date settings is a no-op.
+///
+public static class LegacyPreferencesMigrator
+{
+ /// Current schema version. Bumped whenever a new migration step is added.
+ public const int CURRENT_SETTINGS_VERSION = 1;
+
+ ///
+ /// Migrates the given in place. Returns if any
+ /// migration step was applied (the caller may use this signal to persist the upgraded settings).
+ ///
+ public static bool Migrate (Settings settings)
+ {
+ ArgumentNullException.ThrowIfNull(settings);
+
+ var changed = false;
+
+ if (settings.SettingsVersion < 1)
+ {
+ MigrateToV1(settings);
+ settings.SettingsVersion = 1;
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ private static void MigrateToV1 (Settings settings)
+ {
+ // Existing ColumnizerMaskEntry rows pre-date the per-row Type field — they were regex-only.
+ // Their default-loaded value would be Glob, which would silently change behaviour. Rewrite to Regex.
+ if (settings.Preferences?.ColumnizerMaskList != null)
+ {
+ foreach (var entry in settings.Preferences.ColumnizerMaskList.Where(entry => entry != null))
+ {
+ entry.Type = MaskType.Regex;
+ }
+ }
+
+ // Preserve the deprecated MaskPrio bool's intent on the new enum, but only if the enum is still
+ // at its default — otherwise the user has already chosen.
+#pragma warning disable CS0618 // Migrating away from MaskPrio
+ if (settings.Preferences != null
+ && settings.Preferences.ColumnizerSelectionPriority == ColumnizerSelectionPriority.HistoryThenMask
+ && settings.Preferences.MaskPrio)
+ {
+ settings.Preferences.ColumnizerSelectionPriority = ColumnizerSelectionPriority.MaskThenHistory;
+ }
+#pragma warning restore CS0618
+ }
+}
diff --git a/src/LogExpert.Core/Classes/Columnizer/ColumnizerMaskMatcher.cs b/src/LogExpert.Core/Classes/Columnizer/ColumnizerMaskMatcher.cs
new file mode 100644
index 00000000..a093b643
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Columnizer/ColumnizerMaskMatcher.cs
@@ -0,0 +1,69 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+using LogExpert.Core.Config;
+
+namespace LogExpert.Core.Classes.Columnizer;
+
+///
+/// Pure mask-matching for . Supports glob (*, ?) and
+/// .NET regular expression patterns. Match is case-insensitive (file names on Windows are case-insensitive).
+///
+///
+/// The matcher never throws — malformed input returns . Glob translation rules:
+///
+/// - * → .*
+/// - ? → . (single character)
+/// - Every other character is regex-escaped
+/// - Result is anchored with ^…$
+///
+///
+public static class ColumnizerMaskMatcher
+{
+ private const RegexOptions OPTIONS = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
+
+ public static bool Matches (ColumnizerMaskEntry entry, string fileName)
+ {
+ if (entry == null || string.IsNullOrEmpty(entry.Mask) || string.IsNullOrEmpty(fileName))
+ {
+ return false;
+ }
+
+ var pattern = entry.Type == MaskType.Glob
+ ? GlobToRegex(entry.Mask)
+ : entry.Mask;
+
+ try
+ {
+ return Regex.IsMatch(fileName, pattern, OPTIONS);
+ }
+ catch (ArgumentException)
+ {
+ // Malformed regex (user-supplied) — treat as non-match rather than throwing.
+ return false;
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ return false;
+ }
+ }
+
+ private static string GlobToRegex (string glob)
+ {
+ var sb = new StringBuilder(glob.Length + 4);
+ _ = sb.Append('^');
+
+ foreach (var ch in glob)
+ {
+ _ = ch switch
+ {
+ '*' => sb.Append(".*"),
+ '?' => sb.Append('.'),
+ _ => sb.Append(Regex.Escape(ch.ToString())),
+ };
+ }
+
+ _ = sb.Append('$');
+ return sb.ToString();
+ }
+}
diff --git a/src/LogExpert.Core/Classes/Columnizer/ColumnizerPicker.cs b/src/LogExpert.Core/Classes/Columnizer/ColumnizerPicker.cs
index 3423404f..75349176 100644
--- a/src/LogExpert.Core/Classes/Columnizer/ColumnizerPicker.cs
+++ b/src/LogExpert.Core/Classes/Columnizer/ColumnizerPicker.cs
@@ -18,7 +18,7 @@ public static class ColumnizerPicker
/// Cannot be null.
/// The list of available columnizers to search. Cannot be null.
/// The first columnizer from the list whose name matches the specified value; otherwise, null if no match is found.
- public static ILogLineMemoryColumnizer FindMemorColumnizerByName (string name, IList list)
+ public static ILogLineMemoryColumnizer FindMemoryColumnizerByName (string name, IList list)
{
ArgumentNullException.ThrowIfNull(name, nameof(name));
ArgumentNullException.ThrowIfNull(list, nameof(list));
diff --git a/src/LogExpert.Core/Classes/Columnizer/ColumnizerResolver.cs b/src/LogExpert.Core/Classes/Columnizer/ColumnizerResolver.cs
new file mode 100644
index 00000000..0ab64bb1
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Columnizer/ColumnizerResolver.cs
@@ -0,0 +1,81 @@
+using ColumnizerLib;
+
+using LogExpert.Core.Config;
+
+namespace LogExpert.Core.Classes.Columnizer;
+
+///
+/// Pure precedence-chain resolver that picks a columnizer for a given file from up to four sources: per-file
+/// persistence, columnizer history, columnizer-mask list, and AutoPick.
+///
+///
+/// The exact order of consultation is controlled by . AutoPick fires only when
+/// every other source produced ; it never outranks an explicit Mask, History, or Persistence hit.
+/// Stale Mask entries — those whose is not registered — are
+/// skipped (an optional callback is invoked once per skipped entry) and resolution continues with the next entry in the
+/// list.
+///
+public static class ColumnizerResolver
+{
+ ///
+ /// Returns the winning columnizer for the inputs, or if no source produced a match.
+ ///
+ public static ILogLineMemoryColumnizer? Resolve (ResolveInputs inputs)
+ {
+ ArgumentNullException.ThrowIfNull(inputs);
+
+ // Look up a columnizer by name with "no signal" semantics — returns null when the name is
+ // missing OR not registered, so the precedence chain falls through to the next source.
+ ILogLineMemoryColumnizer? byName (string? name) => string.IsNullOrEmpty(name) ? null : ColumnizerPicker.FindMemoryColumnizerByName(name, inputs.Registered);
+ ILogLineMemoryColumnizer? mask () => TryGetMaskColumnizer(inputs.MaskList, inputs.ShortFileName, inputs.Registered, inputs.OnStaleMaskEntry);
+ ILogLineMemoryColumnizer? history () => byName(inputs.HistoryLookup?.Invoke(inputs.FileName));
+ ILogLineMemoryColumnizer? persistence () => byName(inputs.PersistenceColumnizerName);
+
+ var winner = inputs.Priority switch
+ {
+ ColumnizerSelectionPriority.MaskThenHistory => persistence() ?? mask() ?? history(),
+ ColumnizerSelectionPriority.MaskOverridesPersistence => mask() ?? persistence() ?? history(),
+ ColumnizerSelectionPriority.HistoryThenMask => persistence() ?? history() ?? mask(),
+ _ => persistence() ?? history() ?? mask(),
+ };
+
+ return winner ?? inputs.AutoPick?.Invoke();
+ }
+
+ ///
+ /// Iterates the mask list and returns the first non-stale match. Entries whose columnizer is not registered invoke
+ /// and the iteration continues. Never throws.
+ ///
+ public static ILogLineMemoryColumnizer? TryGetMaskColumnizer (
+ IReadOnlyList maskList,
+ string shortFileName,
+ IList registered,
+ Action? onStale = null)
+ {
+ if (maskList == null || maskList.Count == 0 || string.IsNullOrEmpty(shortFileName))
+ {
+ return null;
+ }
+
+ foreach (var entry in maskList)
+ {
+ if (!ColumnizerMaskMatcher.Matches(entry, shortFileName))
+ {
+ continue;
+ }
+
+
+ // FindMemoryColumnizerByName returns null when the name isn't registered, which is
+ // exactly the "stale entry" signal we need.
+ var columnizer = ColumnizerPicker.FindMemoryColumnizerByName(entry.ColumnizerName, registered);
+ if (columnizer != null)
+ {
+ return columnizer;
+ }
+
+ onStale?.Invoke(entry);
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Columnizer/ResolveInputs.cs b/src/LogExpert.Core/Classes/Columnizer/ResolveInputs.cs
new file mode 100644
index 00000000..a9f5069c
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Columnizer/ResolveInputs.cs
@@ -0,0 +1,36 @@
+using ColumnizerLib;
+
+using LogExpert.Core.Config;
+
+namespace LogExpert.Core.Classes.Columnizer;
+
+///
+/// Inputs to . Designed so the module is exercisable from unit tests with no
+/// dependency on settings storage, plugin registry singletons, or UI.
+///
+public sealed class ResolveInputs
+{
+ public ColumnizerSelectionPriority Priority { get; init; }
+
+ /// The full path or identifier of the file being opened.
+ public string FileName { get; init; } = string.Empty;
+
+ /// The short (filename-only) form of , used for mask matching.
+ public string ShortFileName { get; init; } = string.Empty;
+
+ public IReadOnlyList MaskList { get; init; } = [];
+
+ /// Lookup of a saved history columnizer name for . May be .
+ public Func? HistoryLookup { get; init; }
+
+ /// Columnizer name supplied by the per-file persistence (.lxp). May be .
+ public string? PersistenceColumnizerName { get; init; }
+
+ /// AutoPick callback (e.g. content-based detection). May be .
+ public Func? AutoPick { get; init; }
+
+ public IList Registered { get; init; } = [];
+
+ /// Invoked once for each Mask entry that matched but referenced a missing columnizer.
+ public Action? OnStaleMaskEntry { get; init; }
+}
diff --git a/src/LogExpert.Core/Config/ColumnizerMaskEntry.cs b/src/LogExpert.Core/Config/ColumnizerMaskEntry.cs
index 9e11b1de..3cb32225 100644
--- a/src/LogExpert.Core/Config/ColumnizerMaskEntry.cs
+++ b/src/LogExpert.Core/Config/ColumnizerMaskEntry.cs
@@ -6,4 +6,10 @@ public class ColumnizerMaskEntry
public string ColumnizerName { get; set; }
public string Mask { get; set; }
+
+ ///
+ /// How is interpreted. Defaults to for new entries;
+ /// the settings migrator rewrites pre-1.21 entries to to preserve behaviour.
+ ///
+ public MaskType Type { get; set; } = MaskType.Glob;
}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/ColumnizerSelectionPriority.cs b/src/LogExpert.Core/Config/ColumnizerSelectionPriority.cs
new file mode 100644
index 00000000..7d7a5910
--- /dev/null
+++ b/src/LogExpert.Core/Config/ColumnizerSelectionPriority.cs
@@ -0,0 +1,21 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace LogExpert.Core.Config;
+
+///
+/// Controls the precedence order used by the columnizer resolver when multiple sources
+/// (per-file persistence, history, mask list) could supply a columnizer for a file.
+///
+[JsonConverter(typeof(StringEnumConverter))]
+public enum ColumnizerSelectionPriority
+{
+ /// Persistence → History → Mask → AutoPick (default — preserves legacy behaviour).
+ HistoryThenMask = 0,
+
+ /// Persistence → Mask → History → AutoPick.
+ MaskThenHistory = 1,
+
+ /// Mask → Persistence → History → AutoPick. A matching mask outranks the saved .lxp columnizer.
+ MaskOverridesPersistence = 2,
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/ControlCharSettings.cs b/src/LogExpert.Core/Config/ControlCharSettings.cs
index 8a066476..a379efa0 100644
--- a/src/LogExpert.Core/Config/ControlCharSettings.cs
+++ b/src/LogExpert.Core/Config/ControlCharSettings.cs
@@ -1,11 +1,10 @@
-using System.Collections.Generic;
using System.Drawing;
-using System.Linq;
using Newtonsoft.Json;
namespace LogExpert.Core.Config;
+[Serializable]
public sealed class ControlCharSettings
{
public bool Substitute { get; set; }
@@ -31,9 +30,10 @@ public ControlCharStyle Style
internal static HashSet BuildNonWhitespacePreset ()
{
- return Enumerable.Range(0x00, 0x20)
- .Where(c => c is not 0x09 and not 0x0A and not 0x0D)
- .Append(0x7F)
- .ToHashSet();
+ return
+ [
+ .. Enumerable.Range(0x00, 0x20).Where(c => c is not 0x09 and not 0x0A and not 0x0D),
+ 0x7F,
+ ];
}
}
diff --git a/src/LogExpert.Core/Config/ControlCharStyle.cs b/src/LogExpert.Core/Config/ControlCharStyle.cs
index 5395b219..573533bf 100644
--- a/src/LogExpert.Core/Config/ControlCharStyle.cs
+++ b/src/LogExpert.Core/Config/ControlCharStyle.cs
@@ -1,5 +1,9 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
namespace LogExpert.Core.Config;
+[JsonConverter(typeof(StringEnumConverter))]
public enum ControlCharStyle
{
ControlPictures = 0,
diff --git a/src/LogExpert.Core/Config/MaskType.cs b/src/LogExpert.Core/Config/MaskType.cs
new file mode 100644
index 00000000..9e045aa1
--- /dev/null
+++ b/src/LogExpert.Core/Config/MaskType.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace LogExpert.Core.Config;
+
+///
+/// Determines how a string is interpreted when matching file names.
+///
+/// Serialized by name (e.g. "Glob"/"Regex"); the converter still reads legacy integer values.
+[JsonConverter(typeof(StringEnumConverter))]
+public enum MaskType
+{
+ /// Glob pattern using * and ? wildcards (default for new entries).
+ Glob = 0,
+
+ /// .NET regular expression syntax (legacy default; preserved by migration for pre-existing entries).
+ Regex = 1,
+}
diff --git a/src/LogExpert.Core/Config/MultiFileOption.cs b/src/LogExpert.Core/Config/MultiFileOption.cs
index 7dce5106..05af05d2 100644
--- a/src/LogExpert.Core/Config/MultiFileOption.cs
+++ b/src/LogExpert.Core/Config/MultiFileOption.cs
@@ -1,11 +1,13 @@
-using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
namespace LogExpert.Core.Config;
[Serializable]
+[JsonConverter(typeof(StringEnumConverter))]
public enum MultiFileOption
{
- SingleFiles,
- MultiFile,
- Ask
+ SingleFiles = 0,
+ MultiFile = 1,
+ Ask = 2
}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/Preferences.cs b/src/LogExpert.Core/Config/Preferences.cs
index 152a202c..61668708 100644
--- a/src/LogExpert.Core/Config/Preferences.cs
+++ b/src/LogExpert.Core/Config/Preferences.cs
@@ -1,7 +1,9 @@
using System.Drawing;
+using System.Runtime.Versioning;
using LogExpert.Core.Entities;
using LogExpert.Core.Enums;
+using LogExpert.Core.Helpers;
namespace LogExpert.Core.Config;
@@ -131,8 +133,20 @@ public List HilightGroupList
public int MaximumFilterEntriesDisplayed { get; set; } = 20;
+ ///
+ /// Obsolete: replaced by . Will be removed in 1.50. During settings load,
+ /// migrates a true value here to
+ /// on the new property.
+ ///
+ [Obsolete("Replaced by ColumnizerSelectionPriority; will be removed in 1.50.")]
public bool MaskPrio { get; set; }
+ ///
+ /// Controls the precedence order used to resolve a columnizer for a newly opened file.
+ ///
+ [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
+ public ColumnizerSelectionPriority ColumnizerSelectionPriority { get; set; } = ColumnizerSelectionPriority.HistoryThenMask;
+
public bool AutoPick { get; set; }
//TODO Refactor Enum
@@ -170,7 +184,16 @@ public List HilightGroupList
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
- public Font Font { get; set; }
+ [SupportedOSPlatform("windows")]
+ public Font Font
+ {
+ // Lazily materialize from FontString so callers that build Preferences directly (e.g. tests
+ // that bypass ConfigManager.InitializeFont) get a usable Font without manual setup.
+ // ConfigManager.InitializeFont still goes through the setter to install the canonical
+ // instance during real config load.
+ get => field ??= FontHelper.ParseFontStringOrDefault(FontString);
+ set;
+ }
[Obsolete("This setting is no longer used and will be removed in version 1.50. The 'FontString' will be used for Importing / Exporting the Font")]
public float FontSize { get => field; set => field = MathF.Round(value, 1); } = 9.0f;
diff --git a/src/LogExpert.Core/Config/SessionSaveLocation.cs b/src/LogExpert.Core/Config/SessionSaveLocation.cs
index 2759cb19..bc394a4e 100644
--- a/src/LogExpert.Core/Config/SessionSaveLocation.cs
+++ b/src/LogExpert.Core/Config/SessionSaveLocation.cs
@@ -1,25 +1,27 @@
-using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
namespace LogExpert.Core.Config;
[Serializable]
+[JsonConverter(typeof(StringEnumConverter))]
public enum SessionSaveLocation
{
//Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + Path.DirectorySeparatorChar + "LogExpert"
///
///
///
- DocumentsDir,
+ DocumentsDir = 0,
//same directory as the logfile
- SameDir,
- //uses configured folder to save the session files
+ SameDir = 1,
+ //uses configured folder to save the session files
///
///
///
- OwnDir,
+ OwnDir = 2,
///
///
///
- ApplicationStartupDir,
- LoadedSessionFile
+ ApplicationStartupDir = 3,
+ LoadedSessionFile = 4
}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Config/Settings.cs b/src/LogExpert.Core/Config/Settings.cs
index 73988b4d..a9d62ab6 100644
--- a/src/LogExpert.Core/Config/Settings.cs
+++ b/src/LogExpert.Core/Config/Settings.cs
@@ -11,6 +11,12 @@ public class Settings
{
public Preferences Preferences { get; set; } = new();
+ ///
+ /// Settings schema version. Incremented when introduces a
+ /// migration step. Pre-existing settings files have version 0.
+ ///
+ public int SettingsVersion { get; set; }
+
public RegexHistory RegexHistory { get; set; } = new();
public bool AlwaysOnTop { get; set; }
diff --git a/src/LogExpert.Core/Enums/DragOrientations.cs b/src/LogExpert.Core/Enums/DragOrientations.cs
index 14a00a37..15d525b2 100644
--- a/src/LogExpert.Core/Enums/DragOrientations.cs
+++ b/src/LogExpert.Core/Enums/DragOrientations.cs
@@ -1,8 +1,12 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
namespace LogExpert.Core.Enums;
+[JsonConverter(typeof(StringEnumConverter))]
public enum DragOrientations
{
- Horizontal,
- Vertical,
- InvertedVertical
+ Horizontal = 0,
+ Vertical = 1,
+ InvertedVertical = 2
}
diff --git a/src/LogExpert.Core/Enums/ReaderType.cs b/src/LogExpert.Core/Enums/ReaderType.cs
index a16c38a5..facd26c4 100644
--- a/src/LogExpert.Core/Enums/ReaderType.cs
+++ b/src/LogExpert.Core/Enums/ReaderType.cs
@@ -1,23 +1,27 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
namespace LogExpert.Core.Enums;
///
/// Defines the available stream reader implementations.
///
+[JsonConverter(typeof(StringEnumConverter))]
public enum ReaderType
{
///
/// Direct-read implementation: reads decoded chars directly into pooled blocks via
/// StreamReader.Read(char[], offset, count), eliminating per-line string allocation.
///
- SystemDirect,
+ SystemDirect = 0,
///
/// Legacy reader implementation (original).
///
- Legacy,
+ Legacy = 1,
///
/// System.IO.StreamReader based implementation.
///
- System
+ System = 2
}
diff --git a/src/LogExpert.Core/Helpers/FontHelper.cs b/src/LogExpert.Core/Helpers/FontHelper.cs
new file mode 100644
index 00000000..34f871b0
--- /dev/null
+++ b/src/LogExpert.Core/Helpers/FontHelper.cs
@@ -0,0 +1,29 @@
+using System.Drawing;
+using System.Runtime.Versioning;
+
+namespace LogExpert.Core.Helpers;
+
+public static class FontHelper
+{
+ [SupportedOSPlatform("windows")]
+ public static Font ParseFontStringOrDefault (string fontString)
+ {
+ if (!string.IsNullOrWhiteSpace(fontString))
+ {
+ try
+ {
+ var converter = System.ComponentModel.TypeDescriptor.GetConverter(typeof(Font));
+ if (converter.ConvertFromInvariantString(fontString) is Font parsed)
+ {
+ return parsed;
+ }
+ }
+ catch (Exception ex) when (ex is NotSupportedException or ArgumentException or FormatException)
+ {
+ // Fall back to the default below.
+ }
+ }
+
+ return new Font(FontFamily.GenericMonospace.Name, 9f);
+ }
+}
diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs
index 7821b179..4d43832d 100644
--- a/src/LogExpert.Resources/Resources.Designer.cs
+++ b/src/LogExpert.Resources/Resources.Designer.cs
@@ -5545,6 +5545,33 @@ public static string SettingsDialog_UI_CouldNotCreatePortableMode {
}
}
+ ///
+ /// Looks up a localized string similar to Type.
+ ///
+ public static string SettingsDialog_UI_DataGridView_columnHeaderColumnizerMaskType {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_DataGridView_columnHeaderColumnizerMaskType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Columnizer '{0}' is not installed — this entry will be skipped..
+ ///
+ public static string SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskStale {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskStale", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Glob uses * and ? wildcards. Regex uses .NET regular expression syntax..
+ ///
+ public static string SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskType {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskType", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Columnizer.
///
@@ -5626,6 +5653,15 @@ public static string SettingsDialog_UI_FolderBrowser_folderBrowserWorkingDir {
}
}
+ ///
+ /// Looks up a localized string similar to Columnizer selection priority.
+ ///
+ public static string SettingsDialog_UI_GroupBox_groupBoxColumnizerPriority {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_GroupBox_groupBoxColumnizerPriority", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Appearance.
///
@@ -6221,6 +6257,33 @@ public static string SettingsDialog_UI_RadioButton_radioButtonVerticalMouseDragI
}
}
+ ///
+ /// Looks up a localized string similar to Use history then mask (default).
+ ///
+ public static string SettingsDialog_UI_RadioButton_radioColumnizerPriorityHistoryThenMask {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_RadioButton_radioColumnizerPriorityHistoryThenMask", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Use mask, override per-file persistence.
+ ///
+ public static string SettingsDialog_UI_RadioButton_radioColumnizerPriorityMaskOverridesPersistence {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_RadioButton_radioColumnizerPriorityMaskOverridesPersistence", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Use mask, then history.
+ ///
+ public static string SettingsDialog_UI_RadioButton_radioColumnizerPriorityMaskThenHistory {
+ get {
+ return ResourceManager.GetString("SettingsDialog_UI_RadioButton_radioColumnizerPriorityMaskThenHistory", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to This path is based on the executable and where it has been started from..
///
diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx
index ee5302f3..02c7ca72 100644
--- a/src/LogExpert.Resources/Resources.resx
+++ b/src/LogExpert.Resources/Resources.resx
@@ -885,6 +885,27 @@ Checked tools will appear in the icon bar. All other tools are available in the
Mask has priority before history
+
+ Columnizer selection priority
+
+
+ Use history then mask (default)
+
+
+ Use mask, then history
+
+
+ Use mask, override per-file persistence
+
+
+ Type
+
+
+ Glob uses * and ? wildcards. Regex uses .NET regular expression syntax.
+
+
+ Columnizer '{0}' is not installed — this entry will be skipped.
+
Activate Portable Mode
diff --git a/src/LogExpert.Tests/ColumnizerTests/ColumnizerMaskMatcherTests.cs b/src/LogExpert.Tests/ColumnizerTests/ColumnizerMaskMatcherTests.cs
new file mode 100644
index 00000000..c78b30d6
--- /dev/null
+++ b/src/LogExpert.Tests/ColumnizerTests/ColumnizerMaskMatcherTests.cs
@@ -0,0 +1,106 @@
+using LogExpert.Core.Classes.Columnizer;
+using LogExpert.Core.Config;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.ColumnizerTests;
+
+[TestFixture]
+public class ColumnizerMaskMatcherTests
+{
+ private static ColumnizerMaskEntry Entry (string mask, MaskType type) => new() { Mask = mask, Type = type, ColumnizerName = "X" };
+
+ // Glob tests
+
+ [Test]
+ public void GlobStarLog_MatchesFooLog ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("*.log", MaskType.Glob), "foo.log"), Is.True);
+ }
+
+ [Test]
+ public void GlobStarLog_DoesNotMatchFooTxt ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("*.log", MaskType.Glob), "foo.txt"), Is.False);
+ }
+
+ [Test]
+ public void GlobQuestion_MatchesSingleChar ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("?.log", MaskType.Glob), "a.log"), Is.True);
+ }
+
+ [Test]
+ public void GlobQuestion_DoesNotMatchTwoChars ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("?.log", MaskType.Glob), "aa.log"), Is.False);
+ }
+
+ [Test]
+ public void Glob_IsCaseInsensitive ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("*.LOG", MaskType.Glob), "foo.log"), Is.True);
+ }
+
+ [Test]
+ public void Glob_EscapesRegexMetacharacters ()
+ {
+ // "+" must be treated literally in glob mode
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("foo+bar.log", MaskType.Glob), "foo+bar.log"), Is.True);
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("foo+bar.log", MaskType.Glob), "fooXbar.log"), Is.False);
+ }
+
+ [Test]
+ public void Glob_DotIsLiteral ()
+ {
+ // "my.log" glob: dot must match literal dot, not any char
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("my.log", MaskType.Glob), "my.log"), Is.True);
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("my.log", MaskType.Glob), "myXlog"), Is.False);
+ }
+
+ // Regex tests
+
+ [Test]
+ public void Regex_MatchesAnchoredPattern ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry(@".+\.log$", MaskType.Regex), "foo.log"), Is.True);
+ }
+
+ [Test]
+ public void Regex_DoesNotMatchUnrelated ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry(@".+\.log$", MaskType.Regex), "foo.txt"), Is.False);
+ }
+
+ [Test]
+ public void Regex_MalformedPatternReturnsFalse ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("[", MaskType.Regex), "foo.log"), Is.False);
+ }
+
+ // Null / empty input
+
+ [Test]
+ public void NullMask_ReturnsFalse ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry(null!, MaskType.Glob), "foo.log"), Is.False);
+ }
+
+ [Test]
+ public void EmptyMask_ReturnsFalse ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry(string.Empty, MaskType.Glob), "foo.log"), Is.False);
+ }
+
+ [Test]
+ public void NullFileName_ReturnsFalse ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(Entry("*.log", MaskType.Glob), null!), Is.False);
+ }
+
+ [Test]
+ public void NullEntry_ReturnsFalse ()
+ {
+ Assert.That(ColumnizerMaskMatcher.Matches(null!, "foo.log"), Is.False);
+ }
+}
diff --git a/src/LogExpert.Tests/ColumnizerTests/ColumnizerResolverTests.cs b/src/LogExpert.Tests/ColumnizerTests/ColumnizerResolverTests.cs
new file mode 100644
index 00000000..1ed7cb5e
--- /dev/null
+++ b/src/LogExpert.Tests/ColumnizerTests/ColumnizerResolverTests.cs
@@ -0,0 +1,255 @@
+using ColumnizerLib;
+
+using LogExpert.Core.Classes.Columnizer;
+using LogExpert.Core.Config;
+
+using Moq;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.ColumnizerTests;
+
+[TestFixture]
+public class ColumnizerResolverTests
+{
+ private static ILogLineMemoryColumnizer MakeColumnizer (string name)
+ {
+ var mock = new Mock();
+ _ = mock.Setup(c => c.GetName()).Returns(name);
+ return mock.Object;
+ }
+
+ private static ColumnizerMaskEntry Mask (string glob, string columnizerName) => new() { Mask = glob, Type = MaskType.Glob, ColumnizerName = columnizerName };
+
+ #region HistoryThenMask (default)
+
+ [Test]
+ public void HistoryThenMask_HistoryPresent_HistoryWins ()
+ {
+ var history = MakeColumnizer("HistC");
+ var mask = MakeColumnizer("MaskC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.log", "MaskC")],
+ HistoryLookup = _ => "HistC",
+ Registered = [history, mask],
+ });
+
+ Assert.That(winner, Is.SameAs(history));
+ }
+
+ [Test]
+ public void HistoryThenMask_HistoryAbsent_MaskWins ()
+ {
+ var mask = MakeColumnizer("MaskC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.log", "MaskC")],
+ HistoryLookup = _ => null,
+ Registered = [mask],
+ });
+
+ Assert.That(winner, Is.SameAs(mask));
+ }
+
+ [Test]
+ public void HistoryThenMask_BothAbsent_AutoPickFires ()
+ {
+ var auto = MakeColumnizer("AutoC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ AutoPick = () => auto,
+ Registered = [auto],
+ });
+
+ Assert.That(winner, Is.SameAs(auto));
+ }
+
+ [Test]
+ public void HistoryThenMask_AllSourcesNull_ReturnsNull ()
+ {
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ });
+
+ Assert.That(winner, Is.Null);
+ }
+
+ [Test]
+ public void HistoryThenMask_PersistencePresent_BeatsHistoryAndMask ()
+ {
+ var pers = MakeColumnizer("PersC");
+ var hist = MakeColumnizer("HistC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ PersistenceColumnizerName = "PersC",
+ HistoryLookup = _ => "HistC",
+ Registered = [pers, hist],
+ });
+
+ Assert.That(winner, Is.SameAs(pers));
+ }
+
+ #endregion
+
+ #region MaskThenHistory
+
+ [Test]
+ public void MaskThenHistory_MaskPresent_MaskBeatsHistory ()
+ {
+ var mask = MakeColumnizer("MaskC");
+ var hist = MakeColumnizer("HistC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskThenHistory,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.log", "MaskC")],
+ HistoryLookup = _ => "HistC",
+ Registered = [mask, hist],
+ });
+
+ Assert.That(winner, Is.SameAs(mask));
+ }
+
+ [Test]
+ public void MaskThenHistory_PersistenceStillBeatsMask ()
+ {
+ var pers = MakeColumnizer("PersC");
+ var mask = MakeColumnizer("MaskC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskThenHistory,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.log", "MaskC")],
+ PersistenceColumnizerName = "PersC",
+ Registered = [pers, mask],
+ });
+
+ Assert.That(winner, Is.SameAs(pers));
+ }
+
+ #endregion
+
+ #region MaskOverridesPersistence
+
+ [Test]
+ public void MaskOverridesPersistence_MaskPresent_BeatsPersistence ()
+ {
+ var pers = MakeColumnizer("PersC");
+ var mask = MakeColumnizer("MaskC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskOverridesPersistence,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.log", "MaskC")],
+ PersistenceColumnizerName = "PersC",
+ Registered = [pers, mask],
+ });
+
+ Assert.That(winner, Is.SameAs(mask));
+ }
+
+ [Test]
+ public void MaskOverridesPersistence_MaskAbsent_PersistenceWins ()
+ {
+ var pers = MakeColumnizer("PersC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskOverridesPersistence,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ MaskList = [Mask("*.txt", "Other")], // does not match
+ PersistenceColumnizerName = "PersC",
+ Registered = [pers],
+ });
+
+ Assert.That(winner, Is.SameAs(pers));
+ }
+ #endregion
+
+ #region Stale handling
+
+ [Test]
+ public void StaleEntryFirst_ValidEntryWins_OnStaleInvokedOnce ()
+ {
+ var validC = MakeColumnizer("Valid");
+ var stale = Mask("*.log", "MissingPlugin");
+ var valid = Mask("*.log", "Valid");
+ var staleCallbacks = new List();
+
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskThenHistory,
+ FileName = "foo.log",
+ ShortFileName = "foo.log",
+ MaskList = [stale, valid],
+ Registered = [validC],
+ OnStaleMaskEntry = e => staleCallbacks.Add(e),
+ });
+
+ Assert.That(winner, Is.SameAs(validC));
+ Assert.That(staleCallbacks, Has.Count.EqualTo(1));
+ Assert.That(staleCallbacks[0], Is.SameAs(stale));
+ }
+
+ [Test]
+ public void AllStale_MaskReturnsNull_FallsThroughToAutoPick ()
+ {
+ var auto = MakeColumnizer("AutoC");
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.MaskThenHistory,
+ FileName = "foo.log",
+ ShortFileName = "foo.log",
+ MaskList = [Mask("*.log", "Gone1"), Mask("*.log", "Gone2")],
+ AutoPick = () => auto,
+ Registered = [auto],
+ });
+
+ Assert.That(winner, Is.SameAs(auto));
+ }
+
+ [Test]
+ public void AutoPickDoesNotOverrideExplicitMatch ()
+ {
+ var hist = MakeColumnizer("HistC");
+ var auto = MakeColumnizer("AutoC");
+ var autoFired = false;
+ var winner = ColumnizerResolver.Resolve(new ResolveInputs
+ {
+ Priority = ColumnizerSelectionPriority.HistoryThenMask,
+ FileName = "x.log",
+ ShortFileName = "x.log",
+ HistoryLookup = _ => "HistC",
+ AutoPick = () =>
+ {
+ autoFired = true;
+ return auto;
+ },
+ Registered = [hist, auto],
+ });
+
+ Assert.That(winner, Is.SameAs(hist));
+ Assert.That(autoFired, Is.False);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/LogExpert.Tests/ConfigManagerTests/LegacyPreferencesMigratorTests.cs b/src/LogExpert.Tests/ConfigManagerTests/LegacyPreferencesMigratorTests.cs
new file mode 100644
index 00000000..80f5c7f0
--- /dev/null
+++ b/src/LogExpert.Tests/ConfigManagerTests/LegacyPreferencesMigratorTests.cs
@@ -0,0 +1,78 @@
+using LogExpert.Configuration;
+using LogExpert.Core.Config;
+
+using NUnit.Framework;
+
+namespace LogExpert.Tests.ConfigManagerTests;
+
+[TestFixture]
+public class LegacyPreferencesMigratorTests
+{
+ [Test]
+ public void V0Settings_WithMaskPrioTrue_BecomesMaskThenHistory ()
+ {
+ var settings = new Settings
+ {
+ SettingsVersion = 0,
+ };
+#pragma warning disable CS0618
+ settings.Preferences.MaskPrio = true;
+#pragma warning restore CS0618
+
+ var changed = LegacyPreferencesMigrator.Migrate(settings);
+
+ Assert.That(changed, Is.True);
+ Assert.That(settings.Preferences.ColumnizerSelectionPriority, Is.EqualTo(ColumnizerSelectionPriority.MaskThenHistory));
+ Assert.That(settings.SettingsVersion, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void V0Settings_WithMaskPrioFalse_KeepsDefaultPriority ()
+ {
+ var settings = new Settings { SettingsVersion = 0 };
+
+ _ = LegacyPreferencesMigrator.Migrate(settings);
+
+ Assert.That(settings.Preferences.ColumnizerSelectionPriority, Is.EqualTo(ColumnizerSelectionPriority.HistoryThenMask));
+ Assert.That(settings.SettingsVersion, Is.EqualTo(1));
+ }
+
+ [Test]
+ public void V0Settings_PreExistingMaskEntries_AreRewrittenToRegex ()
+ {
+ var settings = new Settings { SettingsVersion = 0 };
+ settings.Preferences.ColumnizerMaskList.Add(new ColumnizerMaskEntry
+ {
+ Mask = @".+\.log$",
+ ColumnizerName = "JsonColumnizer",
+ // Default-loaded value is Glob (the new default).
+ });
+
+ _ = LegacyPreferencesMigrator.Migrate(settings);
+
+ Assert.That(settings.Preferences.ColumnizerMaskList[0].Type, Is.EqualTo(MaskType.Regex));
+ }
+
+ [Test]
+ public void CurrentSettings_IsNoOp ()
+ {
+ var settings = new Settings { SettingsVersion = LegacyPreferencesMigrator.CURRENT_SETTINGS_VERSION };
+ settings.Preferences.ColumnizerMaskList.Add(new ColumnizerMaskEntry
+ {
+ Mask = "*.log",
+ ColumnizerName = "C",
+ Type = MaskType.Glob,
+ });
+
+ var changed = LegacyPreferencesMigrator.Migrate(settings);
+
+ Assert.That(changed, Is.False);
+ Assert.That(settings.Preferences.ColumnizerMaskList[0].Type, Is.EqualTo(MaskType.Glob));
+ }
+
+ [Test]
+ public void NullSettings_Throws ()
+ {
+ _ = Assert.Throws(() => LegacyPreferencesMigrator.Migrate(null!));
+ }
+}
diff --git a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
index f6a22685..118aef06 100644
--- a/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
+++ b/src/LogExpert.UI/Controls/LogWindow/LogWindow.cs
@@ -5279,7 +5279,7 @@ internal void WritePipeTab (IList lineEntryList, string title)
private static void FilterRestore (LogWindow newWin, PersistenceData persistenceData)
{
newWin.WaitForLoadingFinished();
- var columnizer = ColumnizerPicker.FindMemorColumnizerByName(persistenceData.Columnizer.GetName(), PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
+ var columnizer = ColumnizerPicker.FindMemoryColumnizerByName(persistenceData.Columnizer.GetName(), PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
if (columnizer != null)
{
@@ -6358,6 +6358,24 @@ public void LoadFile (string fileName, EncodingOptions encodingOptions)
SetDefaultHighlightGroup();
}
+ else if (ConfigManager.Settings.Preferences.ColumnizerSelectionPriority == Core.Config.ColumnizerSelectionPriority.MaskOverridesPersistence
+ && !IsTempFile)
+ {
+ // The user opted in to mask-overrides-persistence: the .lxp file's bookmarks, filters,
+ // highlight group, encoding etc. still apply, but the columnizer is replaced by the
+ // mask list's match (if any).
+ var shortName = Util.GetNameFromPath(fileName);
+ var maskColumnizer = _logWindowCoordinator.TryGetMaskColumnizer(shortName);
+ if (maskColumnizer != null)
+ {
+ if (_reloadMemento == null)
+ {
+ maskColumnizer = ColumnizerPicker.CloneMemoryColumnizer(maskColumnizer, ConfigManager.ActiveConfigDir);
+ }
+
+ PreSelectColumnizer(maskColumnizer);
+ }
+ }
// this may be set after loading persistence data
if (_fileNames != null && IsMultiFile)
@@ -6640,7 +6658,7 @@ private void PreSelectColumnizer (ILogLineMemoryColumnizer columnizer)
public void PreSelectColumnizerByName (string columnizerName)
{
- var columnizer = ColumnizerPicker.FindMemorColumnizerByName(columnizerName, PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
+ var columnizer = ColumnizerPicker.FindMemoryColumnizerByName(columnizerName, PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
PreSelectColumnizer(ColumnizerPicker.CloneMemoryColumnizer(columnizer, ConfigManager.ActiveConfigDir));
}
diff --git a/src/LogExpert.UI/Dialogs/Helpers/EmptyImageCell.cs b/src/LogExpert.UI/Dialogs/Helpers/EmptyImageCell.cs
new file mode 100644
index 00000000..e53795f9
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/Helpers/EmptyImageCell.cs
@@ -0,0 +1,12 @@
+namespace LogExpert.UI.Dialogs.Helpers;
+
+///
+/// Image cell whose new-row default is a blank bitmap instead of the framework's "image in error"
+/// glyph, so the columnizer grid's new-row placeholder does not show a red X.
+///
+public sealed class EmptyImageCell : DataGridViewImageCell
+{
+ private static readonly Image _blank = new Bitmap(16, 16);
+
+ public override object DefaultNewRowValue => _blank;
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Dialogs/FileStatus.cs b/src/LogExpert.UI/Dialogs/Helpers/FileStatus.cs
similarity index 93%
rename from src/LogExpert.UI/Dialogs/FileStatus.cs
rename to src/LogExpert.UI/Dialogs/Helpers/FileStatus.cs
index cdf8f891..b40bcf03 100644
--- a/src/LogExpert.UI/Dialogs/FileStatus.cs
+++ b/src/LogExpert.UI/Dialogs/Helpers/FileStatus.cs
@@ -1,4 +1,4 @@
-namespace LogExpert.UI.Dialogs;
+namespace LogExpert.UI.Dialogs.Helpers;
///
/// Represents the status of a file in the missing files dialog.
diff --git a/src/LogExpert.UI/Dialogs/MissingFileItem.cs b/src/LogExpert.UI/Dialogs/Helpers/MissingFileItem.cs
similarity index 68%
rename from src/LogExpert.UI/Dialogs/MissingFileItem.cs
rename to src/LogExpert.UI/Dialogs/Helpers/MissingFileItem.cs
index 73c35497..8827ae39 100644
--- a/src/LogExpert.UI/Dialogs/MissingFileItem.cs
+++ b/src/LogExpert.UI/Dialogs/Helpers/MissingFileItem.cs
@@ -1,19 +1,21 @@
-namespace LogExpert.UI.Dialogs;
+namespace LogExpert.UI.Dialogs.Helpers;
///
/// Represents a file item in the Missing Files Dialog ListView.
///
-public class MissingFileItem
+/// Original path from session file
+/// Current file status
+public class MissingFileItem (string originalPath, FileStatus status)
{
///
/// Original file path from the session/project file.
///
- public string OriginalPath { get; set; }
+ public string OriginalPath { get; set; } = originalPath;
///
/// Current status of the file.
///
- public FileStatus Status { get; set; }
+ public FileStatus Status { get; set; } = status;
///
/// List of alternative paths that might be the same file.
@@ -23,7 +25,7 @@ public class MissingFileItem
///
/// Currently selected path (original or alternative).
///
- public string SelectedPath { get; set; }
+ public string SelectedPath { get; set; } = originalPath;
///
/// Indicates whether the file is accessible.
@@ -46,16 +48,4 @@ public class MissingFileItem
FileStatus.AlternativeSelected => "Alternative Selected",
_ => "Unknown"
};
-
- ///
- /// Constructor for MissingFileItem.
- ///
- /// Original path from session file
- /// Current file status
- public MissingFileItem (string originalPath, FileStatus status)
- {
- OriginalPath = originalPath;
- Status = status;
- SelectedPath = originalPath;
- }
}
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs b/src/LogExpert.UI/Dialogs/Helpers/MissingFilesDialogResult.cs
similarity index 97%
rename from src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs
rename to src/LogExpert.UI/Dialogs/Helpers/MissingFilesDialogResult.cs
index d0ada4ec..6117adcd 100644
--- a/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs
+++ b/src/LogExpert.UI/Dialogs/Helpers/MissingFilesDialogResult.cs
@@ -1,4 +1,4 @@
-namespace LogExpert.UI.Dialogs;
+namespace LogExpert.UI.Dialogs.Helpers;
///
/// Represents the result of the Missing Files Dialog interaction.
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs b/src/LogExpert.UI/Dialogs/Helpers/MissingFilesMessageBox.cs
similarity index 98%
rename from src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs
rename to src/LogExpert.UI/Dialogs/Helpers/MissingFilesMessageBox.cs
index da39de1f..1ef0b8ad 100644
--- a/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs
+++ b/src/LogExpert.UI/Dialogs/Helpers/MissingFilesMessageBox.cs
@@ -3,7 +3,7 @@
using LogExpert.Core.Classes.Persister;
-namespace LogExpert.UI.Dialogs;
+namespace LogExpert.UI.Dialogs.Helpers;
///
/// Temporary helper for showing missing file alerts until full dialog is implemented.
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 0591e4d0..9caadd91 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -17,6 +17,7 @@
using LogExpert.Core.Interfaces;
using LogExpert.Dialogs;
using LogExpert.UI.Dialogs;
+using LogExpert.UI.Dialogs.Helpers;
using LogExpert.UI.Entities;
using LogExpert.UI.Extensions;
using LogExpert.UI.Extensions.LogWindow;
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs
index 04d8b34d..74e85f4e 100644
--- a/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs
+++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs
@@ -2,6 +2,7 @@
using System.Runtime.Versioning;
using LogExpert.Core.Classes.Persister;
+using LogExpert.UI.Dialogs.Helpers;
namespace LogExpert.UI.Dialogs;
diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
index e1027136..cfd9aa09 100644
--- a/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
+++ b/src/LogExpert.UI/Dialogs/SettingsDialog.Designer.cs
@@ -7,24 +7,10 @@ partial class SettingsDialog
///
private System.ComponentModel.IContainer components = null;
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
#region Windows Form Designer generated code
///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
+ /// Required method for Designer support - do not modify the contents of this method with the code editor.
///
private void InitializeComponent ()
{
@@ -101,12 +87,13 @@ private void InitializeComponent ()
labelArguments = new Label();
textBoxArguments = new TextBox();
tabPageColumnizers = new TabPage();
+ groupBoxColumnizerPriority = new GroupBox();
checkBoxAutoPick = new CheckBox();
- checkBoxMaskPrio = new CheckBox();
+ radioColumnizerPriorityHistoryThenMask = new RadioButton();
+ radioColumnizerPriorityMaskThenHistory = new RadioButton();
+ radioColumnizerPriorityMaskOverridesPersistence = new RadioButton();
buttonDelete = new Button();
dataGridViewColumnizer = new DataGridView();
- dataGridViewTextBoxColumnFileMask = new DataGridViewTextBoxColumn();
- dataGridViewComboBoxColumnColumnizer = new DataGridViewComboBoxColumn();
tabPageHighlightMask = new TabPage();
dataGridViewHighlightMask = new DataGridView();
dataGridViewTextBoxColumnFileName = new DataGridViewTextBoxColumn();
@@ -188,6 +175,10 @@ private void InitializeComponent ()
toolTip = new ToolTip(components);
buttonExport = new Button();
buttonImport = new Button();
+ dataGridViewImageColumnColumnizerStale = new DataGridViewImageColumn();
+ dataGridViewTextBoxColumnFileMask = new DataGridViewTextBoxColumn();
+ dataGridViewComboBoxColumnColumnizerMaskType = new DataGridViewComboBoxColumn();
+ dataGridViewComboBoxColumnColumnizer = new DataGridViewComboBoxColumn();
tabControlSettings.SuspendLayout();
tabPageViewSettings.SuspendLayout();
((System.ComponentModel.ISupportInitialize)upDownMaximumLineLength).BeginInit();
@@ -206,6 +197,7 @@ private void InitializeComponent ()
tabPageExternalTools.SuspendLayout();
groupBoxToolSettings.SuspendLayout();
tabPageColumnizers.SuspendLayout();
+ groupBoxColumnizerPriority.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewColumnizer).BeginInit();
tabPageHighlightMask.SuspendLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewHighlightMask).BeginInit();
@@ -1058,8 +1050,7 @@ private void InitializeComponent ()
//
// tabPageColumnizers
//
- tabPageColumnizers.Controls.Add(checkBoxAutoPick);
- tabPageColumnizers.Controls.Add(checkBoxMaskPrio);
+ tabPageColumnizers.Controls.Add(groupBoxColumnizerPriority);
tabPageColumnizers.Controls.Add(buttonDelete);
tabPageColumnizers.Controls.Add(dataGridViewColumnizer);
tabPageColumnizers.Location = new Point(4, 24);
@@ -1071,12 +1062,25 @@ private void InitializeComponent ()
tabPageColumnizers.Text = "Columnizers";
tabPageColumnizers.UseVisualStyleBackColor = true;
//
+ // groupBoxColumnizerPriority
+ //
+ groupBoxColumnizerPriority.Controls.Add(checkBoxAutoPick);
+ groupBoxColumnizerPriority.Controls.Add(radioColumnizerPriorityHistoryThenMask);
+ groupBoxColumnizerPriority.Controls.Add(radioColumnizerPriorityMaskThenHistory);
+ groupBoxColumnizerPriority.Controls.Add(radioColumnizerPriorityMaskOverridesPersistence);
+ groupBoxColumnizerPriority.Location = new Point(4, 339);
+ groupBoxColumnizerPriority.Name = "groupBoxColumnizerPriority";
+ groupBoxColumnizerPriority.Size = new Size(500, 98);
+ groupBoxColumnizerPriority.TabIndex = 4;
+ groupBoxColumnizerPriority.TabStop = false;
+ groupBoxColumnizerPriority.Text = "Columnizer selection priority";
+ //
// checkBoxAutoPick
//
checkBoxAutoPick.AutoSize = true;
checkBoxAutoPick.Checked = true;
checkBoxAutoPick.CheckState = CheckState.Checked;
- checkBoxAutoPick.Location = new Point(530, 386);
+ checkBoxAutoPick.Location = new Point(301, 72);
checkBoxAutoPick.Margin = new Padding(4, 5, 4, 5);
checkBoxAutoPick.Name = "checkBoxAutoPick";
checkBoxAutoPick.Size = new Size(192, 19);
@@ -1084,20 +1088,41 @@ private void InitializeComponent ()
checkBoxAutoPick.Text = "Automatically pick for new files";
checkBoxAutoPick.UseVisualStyleBackColor = true;
//
- // checkBoxMaskPrio
- //
- checkBoxMaskPrio.AutoSize = true;
- checkBoxMaskPrio.Location = new Point(213, 388);
- checkBoxMaskPrio.Margin = new Padding(4, 5, 4, 5);
- checkBoxMaskPrio.Name = "checkBoxMaskPrio";
- checkBoxMaskPrio.Size = new Size(192, 19);
- checkBoxMaskPrio.TabIndex = 4;
- checkBoxMaskPrio.Text = "Mask has priority before history";
- checkBoxMaskPrio.UseVisualStyleBackColor = true;
+ // radioColumnizerPriorityHistoryThenMask
+ //
+ radioColumnizerPriorityHistoryThenMask.AutoSize = true;
+ radioColumnizerPriorityHistoryThenMask.Checked = true;
+ radioColumnizerPriorityHistoryThenMask.Location = new Point(6, 22);
+ radioColumnizerPriorityHistoryThenMask.Name = "radioColumnizerPriorityHistoryThenMask";
+ radioColumnizerPriorityHistoryThenMask.Size = new Size(189, 19);
+ radioColumnizerPriorityHistoryThenMask.TabIndex = 0;
+ radioColumnizerPriorityHistoryThenMask.TabStop = true;
+ radioColumnizerPriorityHistoryThenMask.Text = "Use history then mask (default)";
+ radioColumnizerPriorityHistoryThenMask.UseVisualStyleBackColor = true;
+ //
+ // radioColumnizerPriorityMaskThenHistory
+ //
+ radioColumnizerPriorityMaskThenHistory.AutoSize = true;
+ radioColumnizerPriorityMaskThenHistory.Location = new Point(6, 48);
+ radioColumnizerPriorityMaskThenHistory.Name = "radioColumnizerPriorityMaskThenHistory";
+ radioColumnizerPriorityMaskThenHistory.Size = new Size(144, 19);
+ radioColumnizerPriorityMaskThenHistory.TabIndex = 1;
+ radioColumnizerPriorityMaskThenHistory.Text = "Use mask, then history";
+ radioColumnizerPriorityMaskThenHistory.UseVisualStyleBackColor = true;
+ //
+ // radioColumnizerPriorityMaskOverridesPersistence
+ //
+ radioColumnizerPriorityMaskOverridesPersistence.AutoSize = true;
+ radioColumnizerPriorityMaskOverridesPersistence.Location = new Point(6, 73);
+ radioColumnizerPriorityMaskOverridesPersistence.Name = "radioColumnizerPriorityMaskOverridesPersistence";
+ radioColumnizerPriorityMaskOverridesPersistence.Size = new Size(227, 19);
+ radioColumnizerPriorityMaskOverridesPersistence.TabIndex = 2;
+ radioColumnizerPriorityMaskOverridesPersistence.Text = "Use mask, override per-file persistence";
+ radioColumnizerPriorityMaskOverridesPersistence.UseVisualStyleBackColor = true;
//
// buttonDelete
//
- buttonDelete.Location = new Point(12, 380);
+ buttonDelete.Location = new Point(812, 395);
buttonDelete.Margin = new Padding(4, 5, 4, 5);
buttonDelete.Name = "buttonDelete";
buttonDelete.Size = new Size(112, 35);
@@ -1112,28 +1137,17 @@ private void InitializeComponent ()
dataGridViewColumnizer.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
dataGridViewColumnizer.BackgroundColor = SystemColors.ControlLight;
dataGridViewColumnizer.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;
- dataGridViewColumnizer.Columns.AddRange(new DataGridViewColumn[] { dataGridViewTextBoxColumnFileMask, dataGridViewComboBoxColumnColumnizer });
+ dataGridViewColumnizer.Columns.AddRange(new DataGridViewColumn[] { dataGridViewImageColumnColumnizerStale, dataGridViewTextBoxColumnFileMask, dataGridViewComboBoxColumnColumnizerMaskType, dataGridViewComboBoxColumnColumnizer });
dataGridViewColumnizer.Dock = DockStyle.Top;
dataGridViewColumnizer.EditMode = DataGridViewEditMode.EditOnEnter;
dataGridViewColumnizer.Location = new Point(4, 5);
dataGridViewColumnizer.Margin = new Padding(4, 5, 4, 5);
dataGridViewColumnizer.Name = "dataGridViewColumnizer";
dataGridViewColumnizer.RowHeadersWidth = 62;
- dataGridViewColumnizer.Size = new Size(934, 365);
+ dataGridViewColumnizer.Size = new Size(934, 326);
dataGridViewColumnizer.TabIndex = 2;
- dataGridViewColumnizer.RowsAdded += OnDataGridViewColumnizerRowsAdded;
- //
- // dataGridViewTextBoxColumnFileMask
- //
- dataGridViewTextBoxColumnFileMask.HeaderText = "File name mask (RegEx)";
- dataGridViewTextBoxColumnFileMask.MinimumWidth = 40;
- dataGridViewTextBoxColumnFileMask.Name = "dataGridViewTextBoxColumnFileMask";
- //
- // dataGridViewComboBoxColumnColumnizer
- //
- dataGridViewComboBoxColumnColumnizer.HeaderText = "Columnizer";
- dataGridViewComboBoxColumnColumnizer.MinimumWidth = 230;
- dataGridViewComboBoxColumnColumnizer.Name = "dataGridViewComboBoxColumnColumnizer";
+ dataGridViewColumnizer.DefaultValuesNeeded += OnDataGridViewColumnizerDefaultValuesNeeded;
+ dataGridViewColumnizer.CurrentCellDirtyStateChanged += OnDataGridViewColumnizerCurrentCellDirtyStateChanged;
//
// tabPageHighlightMask
//
@@ -1348,6 +1362,7 @@ private void InitializeComponent ()
columnControlCharEnabled.MinimumWidth = 30;
columnControlCharEnabled.Name = "columnControlCharEnabled";
columnControlCharEnabled.Resizable = DataGridViewTriState.False;
+ columnControlCharEnabled.Width = 30;
//
// columnControlCharHex
//
@@ -2026,6 +2041,37 @@ private void InitializeComponent ()
buttonImport.UseVisualStyleBackColor = true;
buttonImport.Click += OnBtnImportClick;
//
+ // dataGridViewImageColumnColumnizerStale
+ //
+ dataGridViewImageColumnColumnizerStale.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
+ dataGridViewImageColumnColumnizerStale.HeaderText = "";
+ dataGridViewImageColumnColumnizerStale.ImageLayout = DataGridViewImageCellLayout.Zoom;
+ dataGridViewImageColumnColumnizerStale.MinimumWidth = 24;
+ dataGridViewImageColumnColumnizerStale.Name = "dataGridViewImageColumnColumnizerStale";
+ dataGridViewImageColumnColumnizerStale.ReadOnly = true;
+ dataGridViewImageColumnColumnizerStale.Resizable = DataGridViewTriState.False;
+ dataGridViewImageColumnColumnizerStale.Width = 24;
+ //
+ // dataGridViewTextBoxColumnFileMask
+ //
+ dataGridViewTextBoxColumnFileMask.HeaderText = "File name mask";
+ dataGridViewTextBoxColumnFileMask.MinimumWidth = 40;
+ dataGridViewTextBoxColumnFileMask.Name = "dataGridViewTextBoxColumnFileMask";
+ //
+ // dataGridViewComboBoxColumnColumnizerMaskType
+ //
+ dataGridViewComboBoxColumnColumnizerMaskType.AutoSizeMode = DataGridViewAutoSizeColumnMode.None;
+ dataGridViewComboBoxColumnColumnizerMaskType.HeaderText = "Type";
+ dataGridViewComboBoxColumnColumnizerMaskType.MinimumWidth = 80;
+ dataGridViewComboBoxColumnColumnizerMaskType.Name = "dataGridViewComboBoxColumnColumnizerMaskType";
+ dataGridViewComboBoxColumnColumnizerMaskType.Width = 80;
+ //
+ // dataGridViewComboBoxColumnColumnizer
+ //
+ dataGridViewComboBoxColumnColumnizer.HeaderText = "Columnizer";
+ dataGridViewComboBoxColumnColumnizer.MinimumWidth = 230;
+ dataGridViewComboBoxColumnColumnizer.Name = "dataGridViewComboBoxColumnColumnizer";
+ //
// SettingsDialog
//
AcceptButton = buttonOk;
@@ -2073,7 +2119,8 @@ private void InitializeComponent ()
groupBoxToolSettings.ResumeLayout(false);
groupBoxToolSettings.PerformLayout();
tabPageColumnizers.ResumeLayout(false);
- tabPageColumnizers.PerformLayout();
+ groupBoxColumnizerPriority.ResumeLayout(false);
+ groupBoxColumnizerPriority.PerformLayout();
((System.ComponentModel.ISupportInitialize)dataGridViewColumnizer).EndInit();
tabPageHighlightMask.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)dataGridViewHighlightMask).EndInit();
@@ -2141,10 +2188,11 @@ private void InitializeComponent ()
private System.Windows.Forms.TabPage tabPageColumnizers;
private System.Windows.Forms.DataGridView dataGridViewColumnizer;
private System.Windows.Forms.Button buttonDelete;
- private System.Windows.Forms.DataGridViewTextBoxColumn dataGridViewTextBoxColumnFileMask;
- private System.Windows.Forms.DataGridViewComboBoxColumn dataGridViewComboBoxColumnColumnizer;
private System.Windows.Forms.CheckBox checkBoxSysout;
- private System.Windows.Forms.CheckBox checkBoxMaskPrio;
+ private System.Windows.Forms.GroupBox groupBoxColumnizerPriority;
+ private System.Windows.Forms.RadioButton radioColumnizerPriorityHistoryThenMask;
+ private System.Windows.Forms.RadioButton radioColumnizerPriorityMaskThenHistory;
+ private System.Windows.Forms.RadioButton radioColumnizerPriorityMaskOverridesPersistence;
private System.Windows.Forms.GroupBox groupBoxMisc;
private System.Windows.Forms.CheckBox checkBoxAskCloseTabs;
private System.Windows.Forms.TabPage tabPageMultiFile;
@@ -2271,4 +2319,8 @@ private void InitializeComponent ()
private DataGridViewTextBoxColumn columnControlCharAbbr;
private DataGridViewTextBoxColumn columnControlCharCaret;
private DataGridViewTextBoxColumn columnControlCharPreview;
+ private DataGridViewImageColumn dataGridViewImageColumnColumnizerStale;
+ private DataGridViewTextBoxColumn dataGridViewTextBoxColumnFileMask;
+ private DataGridViewComboBoxColumn dataGridViewComboBoxColumnColumnizerMaskType;
+ private DataGridViewComboBoxColumn dataGridViewComboBoxColumnColumnizer;
}
diff --git a/src/LogExpert.UI/Dialogs/SettingsDialog.cs b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
index 09e5097a..5432af0f 100644
--- a/src/LogExpert.UI/Dialogs/SettingsDialog.cs
+++ b/src/LogExpert.UI/Dialogs/SettingsDialog.cs
@@ -6,7 +6,6 @@
using ColumnizerLib;
-using LogExpert.Core.Classes.Columnizer;
using LogExpert.Core.Config;
using LogExpert.Core.Entities;
using LogExpert.Core.Enums;
@@ -14,6 +13,7 @@
using LogExpert.UI.ControlCharDisplay;
using LogExpert.UI.Controls.LogTabWindow;
using LogExpert.UI.Dialogs;
+using LogExpert.UI.Dialogs.Helpers;
using LogExpert.UI.Extensions;
namespace LogExpert.Dialogs;
@@ -26,6 +26,7 @@ internal partial class SettingsDialog : Form
#region Fields
private readonly Image _emptyImage = new Bitmap(16, 16);
+ private readonly Image _staleImage = SystemIcons.Exclamation.ToBitmap();
private readonly LogTabWindow _logTabWin;
private const float DEFAULT_FONT_SIZE = 9.0f;
@@ -74,6 +75,8 @@ private SettingsDialog (Preferences prefs, LogTabWindow logTabWin)
InitializeComponent();
+ dataGridViewImageColumnColumnizerStale.CellTemplate = new EmptyImageCell();
+
LoadResources();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
@@ -81,6 +84,23 @@ private SettingsDialog (Preferences prefs, LogTabWindow logTabWin)
ResumeLayout();
}
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose (bool disposing)
+ {
+ if (disposing)
+ {
+ components?.Dispose();
+ _staleImage?.Dispose();
+ _emptyImage?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
public SettingsDialog (Preferences prefs, LogTabWindow logTabWin, int tabToOpen, IConfigManager configManager) : this(prefs, logTabWin)
{
tabControlSettings.SelectedIndex = tabToOpen;
@@ -139,6 +159,8 @@ private void ApplyTextResources ()
dataGridViewTextBoxColumnFileMask.HeaderText = Resources.SettingsDialog_UI_DataGridViewTextBoxColumn_FileMask;
dataGridViewComboBoxColumnColumnizer.HeaderText = Resources.SettingsDialog_UI_DataGridViewComboBoxColumn_Columnizer;
+ dataGridViewComboBoxColumnColumnizerMaskType.HeaderText = Resources.SettingsDialog_UI_DataGridView_columnHeaderColumnizerMaskType;
+ dataGridViewComboBoxColumnColumnizerMaskType.ToolTipText = Resources.SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskType;
dataGridViewTextBoxColumnFileName.HeaderText = Resources.SettingsDialog_UI_DataGridViewTextBoxColumn_FileName;
dataGridViewComboBoxColumnHighlightGroup.HeaderText = Resources.SettingsDialog_UI_DataGridViewComboBoxColumn_HighlightGroup;
}
@@ -212,8 +234,6 @@ private void FillDialog ()
break;
}
case SessionSaveLocation.LoadedSessionFile:
- // intentionally left blank
- break;
default:
// intentionally left blank
break;
@@ -252,18 +272,30 @@ private void FillDialog ()
FillEncodingList();
FillLanguageList();
FillReaderTypeList();
+ FillControlCharsTab();
comboBoxEncoding.SelectedItem = Encoding.GetEncoding(Preferences.DefaultEncoding);
comboBoxLanguage.SelectedItem = CultureInfo.GetCultureInfo(Preferences.DefaultLanguage).Name;
- checkBoxMaskPrio.Checked = Preferences.MaskPrio;
+ switch (Preferences.ColumnizerSelectionPriority)
+ {
+ case ColumnizerSelectionPriority.MaskThenHistory:
+ radioColumnizerPriorityMaskThenHistory.Checked = true;
+ break;
+ case ColumnizerSelectionPriority.MaskOverridesPersistence:
+ radioColumnizerPriorityMaskOverridesPersistence.Checked = true;
+ break;
+ case ColumnizerSelectionPriority.HistoryThenMask:
+ default:
+ radioColumnizerPriorityHistoryThenMask.Checked = true;
+ break;
+ }
+
checkBoxAutoPick.Checked = Preferences.AutoPick;
checkBoxAskCloseTabs.Checked = Preferences.AskForClose;
checkBoxColumnFinder.Checked = Preferences.ShowColumnFinder;
checkBoxShowErrorMessageOnlyOneInstance.Checked = Preferences.ShowErrorMessageAllowOnlyOneInstances;
-
- FillControlCharsTab();
}
private void FillReaderTypeList ()
@@ -313,65 +345,6 @@ private void SaveMultifileData ()
Preferences.MultiFileOptions.MaxDayTry = (int)upDownMultifileDays.Value;
}
- private static void OnBtnToolClickInternal (TextBox textBox)
- {
- OpenFileDialog dlg = new()
- {
- InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
- };
-
- if (!string.IsNullOrEmpty(textBox.Text))
- {
- FileInfo info = new(textBox.Text);
- if (info.Directory != null && info.Directory.Exists)
- {
- dlg.InitialDirectory = info.DirectoryName;
- }
- }
-
- if (dlg.ShowDialog() == DialogResult.OK)
- {
- textBox.Text = dlg.FileName;
- }
- }
-
- //TODO: what is the purpose of this method?
- private void OnBtnArgsClickInternal (TextBox textBox)
- {
- ToolArgsDialog dlg = new(_logTabWin, this)
- {
- Arg = textBox.Text
- };
-
- if (dlg.ShowDialog() == DialogResult.OK)
- {
- textBox.Text = dlg.Arg;
- }
- }
-
- private static void OnBtnWorkingDirClick (TextBox textBox)
- {
- FolderBrowserDialog dlg = new()
- {
- RootFolder = Environment.SpecialFolder.MyComputer,
- Description = Resources.SettingsDialog_UI_FolderBrowser_folderBrowserWorkingDir
- };
-
- if (!string.IsNullOrEmpty(textBox.Text))
- {
- DirectoryInfo info = new(textBox.Text);
- if (info.Exists)
- {
- dlg.SelectedPath = info.FullName;
- }
- }
-
- if (dlg.ShowDialog() == DialogResult.OK)
- {
- textBox.Text = dlg.SelectedPath;
- }
- }
-
private void FillColumnizerForToolsList ()
{
if (_selectedTool != null)
@@ -395,9 +368,6 @@ private static void FillColumnizerForToolsList (ComboBox comboBox, string column
}
}
- //ILogLineColumnizer columnizer = Util.FindColumnizerByName(columnizerName, this.logTabWin.RegisteredColumnizers);
- //if (columnizer == null)
- // columnizer = this.logTabWin.RegisteredColumnizers[0];
comboBox.SelectedIndex = selIndex;
}
@@ -405,46 +375,51 @@ private void FillColumnizerList ()
{
dataGridViewColumnizer.Rows.Clear();
- var comboColumn = (DataGridViewComboBoxColumn)dataGridViewColumnizer.Columns[1];
+ var comboColumn = (DataGridViewComboBoxColumn)dataGridViewColumnizer.Columns[3];
comboColumn.Items.Clear();
- //var textColumn = (DataGridViewTextBoxColumn)dataGridViewColumnizer.Columns[0];
+ var typeColumn = (DataGridViewComboBoxColumn)dataGridViewColumnizer.Columns[2];
+ typeColumn.ValueType = typeof(MaskType);
+
+ if (typeColumn.Items.Count == 0)
+ {
+ _ = typeColumn.Items.Add(MaskType.Glob);
+ _ = typeColumn.Items.Add(MaskType.Regex);
+ }
var columnizers = PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers;
+ var columnizerLookup = new HashSet(StringComparer.Ordinal);
foreach (var columnizer in columnizers)
{
- _ = comboColumn.Items.Add(columnizer.GetName());
+ var name = columnizer.GetName();
+ _ = comboColumn.Items.Add(name);
+ _ = columnizerLookup.Add(name);
}
- //comboColumn.DisplayMember = "Name";
- //comboColumn.ValueMember = "Columnizer";
foreach (var maskEntry in Preferences.ColumnizerMaskList)
{
- DataGridViewRow row = new();
- _ = row.Cells.Add(new DataGridViewTextBoxCell());
- DataGridViewComboBoxCell cell = new();
+ int rowIndex = dataGridViewColumnizer.Rows.Add();
+ var row = dataGridViewColumnizer.Rows[rowIndex];
+
+ row.Cells[1].Value = maskEntry.Mask;
+ row.Cells[2].Value = maskEntry.Type;
- foreach (var logColumnizer in columnizers)
+ if (columnizerLookup.Contains(maskEntry.ColumnizerName))
{
- _ = cell.Items.Add(logColumnizer.GetName());
+ row.Cells[3].Value = maskEntry.ColumnizerName;
+ row.Cells[0].Value = _emptyImage;
+ }
+ else
+ {
+ // Stale entry — the columnizer is not registered. Mark the row but keep the original name
+ // (a future re-install of the plugin will resurrect the entry).
+ row.Cells[3].Value = maskEntry.ColumnizerName;
+ row.Cells[0].Value = _staleImage;
+ row.Cells[0].ToolTipText = string.Format(CultureInfo.CurrentCulture,
+ Resources.SettingsDialog_UI_DataGridView_columnTooltipColumnizerMaskStale,
+ maskEntry.ColumnizerName);
}
-
- _ = row.Cells.Add(cell);
- row.Cells[0].Value = maskEntry.Mask;
- var columnizer = ColumnizerPicker.DecideMemoryColumnizerByName(maskEntry.ColumnizerName,
- PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers);
-
- row.Cells[1].Value = columnizer.GetName();
- _ = dataGridViewColumnizer.Rows.Add(row);
- }
-
- var count = dataGridViewColumnizer.RowCount;
-
- if (count > 0 && !dataGridViewColumnizer.Rows[count - 1].IsNewRow)
- {
- var comboCell = (DataGridViewComboBoxCell)dataGridViewColumnizer.Rows[count - 1].Cells[1];
- comboCell.Value = comboCell.Items[0];
}
}
@@ -477,9 +452,9 @@ private void FillHighlightMaskList ()
_ = row.Cells.Add(cell);
row.Cells[0].Value = maskEntry.Mask;
- var currentGroup = _logTabWin.FindHighlightGroup(maskEntry.HighlightGroupName);
+ //var currentGroup = _logTabWin.FindHighlightGroup(maskEntry.HighlightGroupName);
var highlightGroupList = _logTabWin.HighlightGroupList;
- currentGroup = highlightGroupList.Count > 0 ? highlightGroupList[0] : new HighlightGroup();
+ var currentGroup = highlightGroupList.Count > 0 ? highlightGroupList[0] : new HighlightGroup();
row.Cells[1].Value = currentGroup.GroupName;
_ = dataGridViewHighlightMask.Rows.Add(row);
@@ -489,8 +464,7 @@ private void FillHighlightMaskList ()
if (count > 0 && !dataGridViewHighlightMask.Rows[count - 1].IsNewRow)
{
- var comboCell =
- (DataGridViewComboBoxCell)dataGridViewHighlightMask.Rows[count - 1].Cells[1];
+ var comboCell = (DataGridViewComboBoxCell)dataGridViewHighlightMask.Rows[count - 1].Cells[1];
comboCell.Value = comboCell.Items[0];
}
}
@@ -503,11 +477,17 @@ private void SaveColumnizerList ()
{
if (!row.IsNewRow)
{
+ var type = row.Cells[2].Value is MaskType maskType
+ ? maskType
+ : MaskType.Glob;
+
ColumnizerMaskEntry entry = new()
{
- Mask = (string)row.Cells[0].Value,
- ColumnizerName = (string)row.Cells[1].Value
+ Mask = (string)row.Cells[1].Value,
+ Type = type,
+ ColumnizerName = (string)row.Cells[3].Value
};
+
Preferences.ColumnizerMaskList.Add(entry);
}
}
@@ -526,6 +506,7 @@ private void SaveHighlightMaskList ()
Mask = (string)row.Cells[0].Value,
HighlightGroupName = (string)row.Cells[1].Value
};
+
Preferences.HighlightMaskList.Add(entry);
}
}
@@ -774,7 +755,11 @@ private void OnBtnOkClick (object sender, EventArgs e)
SaveColumnizerList();
- Preferences.MaskPrio = checkBoxMaskPrio.Checked;
+ Preferences.ColumnizerSelectionPriority = radioColumnizerPriorityMaskOverridesPersistence.Checked
+ ? ColumnizerSelectionPriority.MaskOverridesPersistence
+ : radioColumnizerPriorityMaskThenHistory.Checked
+ ? ColumnizerSelectionPriority.MaskThenHistory
+ : ColumnizerSelectionPriority.HistoryThenMask;
Preferences.AutoPick = checkBoxAutoPick.Checked;
Preferences.AskForClose = checkBoxAskCloseTabs.Checked;
Preferences.AllowOnlyOneInstance = checkBoxSingleInstance.Checked;
@@ -823,22 +808,54 @@ private void OnBtnOkClick (object sender, EventArgs e)
private void OnBtnToolClick (object sender, EventArgs e)
{
- OnBtnToolClickInternal(textBoxTool);
+ using OpenFileDialog dlg = new()
+ {
+ InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)
+ };
+
+ if (!string.IsNullOrEmpty(textBoxTool.Text))
+ {
+ FileInfo info = new(textBoxTool.Text);
+ if (info.Directory != null && info.Directory.Exists)
+ {
+ dlg.InitialDirectory = info.DirectoryName;
+ }
+ }
+
+ if (dlg.ShowDialog() == DialogResult.OK)
+ {
+ textBoxTool.Text = dlg.FileName;
+ }
}
- //TODO: what is the purpose of this click?
private void OnBtnArgClick (object sender, EventArgs e)
{
- OnBtnArgsClickInternal(textBoxArguments);
+ using ToolArgsDialog dlg = new(_logTabWin, this)
+ {
+ Arg = textBoxArguments.Text
+ };
+
+ if (dlg.ShowDialog() == DialogResult.OK)
+ {
+ textBoxArguments.Text = dlg.Arg;
+ }
}
- //TODO Remove or refactor this function
- private void OnDataGridViewColumnizerRowsAdded (object sender, DataGridViewRowsAddedEventArgs e)
+ ///
+ /// Adds default values to the Columnizer Grid when a new row is created. The default mask type is set to "Glob",
+ /// and if there are any registered columnizers, the first one in the list is selected as the default columnizer for
+ /// the new row.
+ ///
+ ///
+ ///
+ private void OnDataGridViewColumnizerDefaultValuesNeeded (object sender, DataGridViewRowEventArgs e)
{
- var comboCell = (DataGridViewComboBoxCell)dataGridViewColumnizer.Rows[e.RowIndex].Cells[1];
- if (comboCell.Items.Count > 0)
+ e.Row.Cells[2].Value = MaskType.Glob;
+
+ var columnizers = PluginRegistry.PluginRegistry.Instance.RegisteredColumnizers;
+ if (columnizers.Count > 0)
{
- //comboCell.Value = comboCell.Items[0];
+ e.Row.Cells[3].Value = columnizers[0].GetName();
}
}
@@ -857,6 +874,14 @@ private void OnDataGridViewColumnizerDataError (object sender, DataGridViewDataE
e.Cancel = true;
}
+ private void OnDataGridViewColumnizerCurrentCellDirtyStateChanged (object sender, EventArgs e)
+ {
+ if (dataGridViewColumnizer.IsCurrentCellDirty)
+ {
+ _ = dataGridViewColumnizer.CommitEdit(DataGridViewDataErrorContexts.Commit);
+ }
+ }
+
private void OnChkBoxSysoutCheckedChanged (object sender, EventArgs e)
{
comboBoxColumnizer.Enabled = checkBoxSysout.Checked;
@@ -1170,7 +1195,25 @@ private void OnBtnCancelClick (object sender, EventArgs e)
private void OnBtnWorkingDirClick (object sender, EventArgs e)
{
- OnBtnWorkingDirClick(textBoxWorkingDir);
+ using FolderBrowserDialog dlg = new()
+ {
+ RootFolder = Environment.SpecialFolder.MyComputer,
+ Description = Resources.SettingsDialog_UI_FolderBrowser_folderBrowserWorkingDir
+ };
+
+ if (!string.IsNullOrEmpty(textBoxWorkingDir.Text))
+ {
+ DirectoryInfo info = new(textBoxWorkingDir.Text);
+ if (info.Exists)
+ {
+ dlg.SelectedPath = info.FullName;
+ }
+ }
+
+ if (dlg.ShowDialog() == DialogResult.OK)
+ {
+ textBoxWorkingDir.Text = dlg.SelectedPath;
+ }
}
[SupportedOSPlatform("windows")]
@@ -1323,6 +1366,10 @@ private Dictionary GetToolTipMap ()
};
}
+ #endregion
+
+ #region Control Chars Tab
+
private void FillControlCharsTab ()
{
var s = Preferences.ControlCharSettings ??= new ControlCharSettings();
@@ -1336,11 +1383,32 @@ private void FillControlCharsTab ()
switch (s.Style)
{
- case ControlCharStyle.Caret: radioButtonControlCharStyleCaret.Checked = true; break;
- case ControlCharStyle.CEscape: radioButtonControlCharStyleCEscape.Checked = true; break;
- case ControlCharStyle.Abbreviation: radioButtonControlCharStyleAbbreviation.Checked = true; break;
- case ControlCharStyle.Iso2047: radioButtonControlCharStyleIso2047.Checked = true; break;
- default: radioButtonControlCharStyleControlPictures.Checked = true; break;
+ case ControlCharStyle.Caret:
+ {
+ radioButtonControlCharStyleCaret.Checked = true;
+ break;
+ }
+ case ControlCharStyle.CEscape:
+ {
+ radioButtonControlCharStyleCEscape.Checked = true;
+ break;
+ }
+ case ControlCharStyle.Abbreviation:
+ {
+ radioButtonControlCharStyleAbbreviation.Checked = true;
+ break;
+ }
+ case ControlCharStyle.Iso2047:
+ {
+ radioButtonControlCharStyleIso2047.Checked = true;
+ break;
+ }
+ case ControlCharStyle.ControlPictures:
+ default:
+ {
+ radioButtonControlCharStyleControlPictures.Checked = true;
+ break;
+ }
}
_controlCharsEnabledByCp.Clear();
diff --git a/src/LogExpert.UI/Interface/ILogWindowCoordinator.cs b/src/LogExpert.UI/Interface/ILogWindowCoordinator.cs
index 74390967..2e380126 100644
--- a/src/LogExpert.UI/Interface/ILogWindowCoordinator.cs
+++ b/src/LogExpert.UI/Interface/ILogWindowCoordinator.cs
@@ -31,12 +31,19 @@ internal interface ILogWindowCoordinator
///
/// Resolves the appropriate columnizer for the given file name.
- /// Respects MaskPrio preference (mask-first vs history-first).
- /// Cleans up stale history entries.
- /// Returns null when no match found.
+ /// Honours the configured .
+ /// Cleans up stale history entries. Returns null when no source produced a match.
///
ILogLineMemoryColumnizer? ResolveColumnizer (string fileName);
+ ///
+ /// Returns the mask-list columnizer that matches , ignoring history,
+ /// persistence, and AutoPick. Used by the per-file load path when the user has opted in to
+ /// MaskOverridesPersistence. Returns null when no mask matches or the matching entry's
+ /// columnizer is not registered.
+ ///
+ ILogLineMemoryColumnizer? TryGetMaskColumnizer (string shortFileName);
+
///
/// Shared search parameters across all tabs.
/// All tabs read/write from the same instance.
diff --git a/src/LogExpert.UI/Services/LogWindowCoordinatorService/LogWindowCoordinator.cs b/src/LogExpert.UI/Services/LogWindowCoordinatorService/LogWindowCoordinator.cs
index 76f95e16..c4a15d91 100644
--- a/src/LogExpert.UI/Services/LogWindowCoordinatorService/LogWindowCoordinator.cs
+++ b/src/LogExpert.UI/Services/LogWindowCoordinatorService/LogWindowCoordinator.cs
@@ -120,31 +120,33 @@ public HighlightGroup ResolveHighlightGroup (string? groupName, string? fileName
var preferences = _configManager.Settings.Preferences;
var shortName = Util.GetNameFromPath(fileName);
- return preferences.MaskPrio
- ? FindColumnizerByFileMask(shortName) ?? GetColumnizerHistoryEntry(fileName)
- : GetColumnizerHistoryEntry(fileName) ?? FindColumnizerByFileMask(shortName);
- }
+ // History lookup also cleans up stale entries — preserve that side-effect by routing through
+ // GetColumnizerHistoryEntry rather than a pure name lookup inside ColumnizerResolver.
+ var historyHit = GetColumnizerHistoryEntry(fileName);
- private ILogLineMemoryColumnizer? FindColumnizerByFileMask (string fileName)
- {
- foreach (var entry in _configManager.Settings.Preferences.ColumnizerMaskList.Where(entry => entry.Mask != null))
+ var inputs = new ResolveInputs
{
- try
- {
- if (Regex.IsMatch(fileName, entry.Mask))
- {
- return ColumnizerPicker.FindMemorColumnizerByName(
- entry.ColumnizerName,
- _pluginRegistry.RegisteredColumnizers);
- }
- }
- catch (ArgumentException e)
- {
- _logger.Error($"RegEx-error while finding columnizer: {e}");
- }
- }
+ Priority = preferences.ColumnizerSelectionPriority,
+ FileName = fileName,
+ ShortFileName = shortName,
+ MaskList = preferences.ColumnizerMaskList,
+ HistoryLookup = _ => historyHit?.GetName(),
+ Registered = _pluginRegistry.RegisteredColumnizers,
+ OnStaleMaskEntry = entry => _logger.Warn(
+ $"Columnizer mask '{entry.Mask}' matched '{shortName}' but its columnizer '{entry.ColumnizerName}' is not registered — skipping."),
+ };
+
+ return ColumnizerResolver.Resolve(inputs);
+ }
- return null;
+ public ILogLineMemoryColumnizer? TryGetMaskColumnizer (string shortFileName)
+ {
+ return ColumnizerResolver.TryGetMaskColumnizer(
+ _configManager.Settings.Preferences.ColumnizerMaskList,
+ shortFileName,
+ _pluginRegistry.RegisteredColumnizers,
+ entry => _logger.Warn(
+ $"Columnizer mask '{entry.Mask}' matched '{shortFileName}' but its columnizer '{entry.ColumnizerName}' is not registered — skipping."));
}
private ILogLineMemoryColumnizer? GetColumnizerHistoryEntry (string fileName)
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index e4150631..e09a2549 100644
--- a/src/PluginRegistry/PluginHashGenerator.Generated.cs
+++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs
@@ -10,36 +10,35 @@ public static partial class PluginValidator
{
///
/// Gets pre-calculated SHA256 hashes for built-in plugins.
- /// Generated: 2026-05-22 18:40:00 UTC
+ /// Generated: 2026-05-28 08:22:43 UTC
/// Configuration: Release
- /// Plugin count: 22
+ /// Plugin count: 21
///
public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "1C3A220CDF6CBDF89A8C4355EB258071AD1DDC9D8AF17561BE5681624078FB38",
+ ["AutoColumnizer.dll"] = "6E377F0AE721301F525FCEA7CD9B7FA30DB329BDB17E96F334EE6FBF1251502B",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "C92D60E9989E66E0BE4172A0061DEFA1FAE0CF29A223A30A05293079906890FC",
- ["CsvColumnizer.dll (x86)"] = "C92D60E9989E66E0BE4172A0061DEFA1FAE0CF29A223A30A05293079906890FC",
- ["DefaultPlugins.dll"] = "B5EE5C99499980A5C7C7D1ED23A8FA65E5DCDE6E9261D4D6C0E58CDD05ABE326",
- ["FlashIconHighlighter.dll"] = "3F3224E917015205CDB28F5598CEE059E6E4A94D7D49F05613318ACE6B52FEF8",
- ["GlassfishColumnizer.dll"] = "014926AD769F27DF892535F09205A003B864A410E98767A4F6F58CFD3DC1CB75",
- ["JsonColumnizer.dll"] = "0EB6881F88C248D0786BAAAC4951AD7998E415790DF1118F3E02715476230A47",
- ["JsonCompactColumnizer.dll"] = "A5430CBBACE8A4BB2A706F06DB0B0576D9C6EEDC51732D93CA2F4C2EF02D8836",
- ["Log4jXmlColumnizer.dll"] = "BCA0C165D3D43FFCEA73DC1D01B0166D27C58416A04AF860846CB4059DAF299C",
- ["LogExpert.Core.dll"] = "9E33740726A486E43D262EA07EAD4327D0A8C4C0C09FE7BFE1106AA18358CA7D",
- ["LogExpert.Resources.dll"] = "10570A3ABE83A0997AD457333D36359EADE0296A57054BC931338952E3190656",
+ ["CsvColumnizer.dll"] = "CE335CCA69ACAA72F71B7AD63BF09CAAC3C9492A807B4DDD8053C062AB6433EA",
+ ["CsvColumnizer.dll (x86)"] = "CE335CCA69ACAA72F71B7AD63BF09CAAC3C9492A807B4DDD8053C062AB6433EA",
+ ["DefaultPlugins.dll"] = "9F0DDA2C2064BA5FBD0A68DF31EA29A7D30C2462A5742F27908C093CEF2AA7F9",
+ ["FlashIconHighlighter.dll"] = "83D60C9E885639193A9F609EB94BB735709D95B40A0CB904998AACA51D0D3016",
+ ["GlassfishColumnizer.dll"] = "2E8601C7A6BBCF2B76EC50850B9F03503FBF336F8332FF3365757B298FFEC94D",
+ ["JsonColumnizer.dll"] = "6596ED4BEAB67DABD89E8C4D8AE028A67B029594B706CAD0F652BF774EAD212A",
+ ["JsonCompactColumnizer.dll"] = "12553DC6DE8AE0F688707473608D2D474554C3EBB0FAE96E76A7D37FDEB3CEAE",
+ ["Log4jXmlColumnizer.dll"] = "B7AB67C37465E50C7FA6F640C7C410664F087C0411D5217B88BEEF91766A45D2",
+ ["LogExpert.Resources.dll"] = "8D2E41CADDFB81E03E6F2B2D48059CFABF7DB69BE3E33D6242115E5040BF6356",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.DependencyInjection.Abstractions.dll (x86)"] = "67FA4325000DB017DC0C35829B416F024F042D24EFB868BCF17A895EE6500A93",
["Microsoft.Extensions.Logging.Abstractions.dll"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
["Microsoft.Extensions.Logging.Abstractions.dll (x86)"] = "BB853130F5AFAF335BE7858D661F8212EC653835100F5A4E3AA2C66A4D4F685D",
- ["RegexColumnizer.dll"] = "A66E6F9A81CB856564D6DBD28B435C2AB953E09935A7450D3885A05672FF64D1",
- ["SftpFileSystem.dll"] = "6A5A95C3E05305EF4A5B32E33DA466D8B4B4E3275E35D2B33F3347B561E6D7FB",
- ["SftpFileSystem.dll (x86)"] = "91C022E72324EAD1467C55C853D099B7B05D319174F916315B6C63FA6099D9FD",
- ["SftpFileSystem.Resources.dll"] = "CEBA933FB478A400A46865855807D18C10D1E3855014D1D75C3987E28CF5B36D",
- ["SftpFileSystem.Resources.dll (x86)"] = "CEBA933FB478A400A46865855807D18C10D1E3855014D1D75C3987E28CF5B36D",
+ ["RegexColumnizer.dll"] = "AF3A7237BA515877D9A0D0C6814608B653790892ACBFC844BC031DDF8FD91D25",
+ ["SftpFileSystem.dll"] = "0B3DA99546B5319BA2BE407BC8D7ED65B4AF9E9E133C33B6F6DC315E15E9B8FB",
+ ["SftpFileSystem.dll (x86)"] = "28B9378D8E6D1EFD6257B61B1281A2870A94091E942DFC50DD373A4E684C47D7",
+ ["SftpFileSystem.Resources.dll"] = "DFD0ED9BB470D3EBC739E36386D222D51A6772FFEB7AEE7305E4A65789B5A98B",
+ ["SftpFileSystem.Resources.dll (x86)"] = "DFD0ED9BB470D3EBC739E36386D222D51A6772FFEB7AEE7305E4A65789B5A98B",
};
}
diff --git a/src/PluginRegistry/PluginRegistry.cs b/src/PluginRegistry/PluginRegistry.cs
index f007c384..3eb28c49 100644
--- a/src/PluginRegistry/PluginRegistry.cs
+++ b/src/PluginRegistry/PluginRegistry.cs
@@ -71,7 +71,6 @@ private PluginRegistry (string applicationConfigurationFolder, int pollingInterv
_applicationConfigurationFolder = applicationConfigurationFolder;
PollingInterval = pollingInterval;
- // Initialize Priority 3 & 4 components
_pluginLoader = new DefaultPluginLoader();
_eventBus = new PluginEventBus();
@@ -121,9 +120,6 @@ public static void RegisterAssemblyResolver ()
{
AppDomain.CurrentDomain.AssemblyResolve -= ColumnizerResolveEventHandler;
AppDomain.CurrentDomain.AssemblyResolve += ColumnizerResolveEventHandler;
-
- //// Wire up the converter's assembly loader to go through validated loading
- //LogExpert.Core.Classes.JsonConverters.ColumnizerJsonConverter.AssemblyLoader = LoadAndValidateAssembly;
}
#endregion
@@ -148,7 +144,9 @@ public IList RegisteredColumnizers
foreach (var loader in _lazyColumnizers.ToList())
{
var instance = loader.GetInstance();
- if (instance != null && !field.Contains(instance))
+ // Data duplication by type, not by reference: GetInstance() returns a freshly constructed
+ // object, so Contains() (reference equality) would never catch a same-type duplicate.
+ if (instance != null && !field.Any(c => c.GetType() == instance.GetType()))
{
field.Add(instance);
InitializePluginIfNeeded(instance, loader.Manifest, loader.DllPath);
@@ -872,6 +870,15 @@ private void ProcessLoadedPlugin (object plugin, PluginManifest? manifest, strin
return;
}
+ // Skip if a columnizer of this type is already registered. RegisteredColumnizers holds one
+ // template instance per type, so a same-type entry is always a duplicate (e.g. a built-in
+ // columnizer rediscovered because its defining assembly was scanned).
+ if (RegisteredColumnizers.Any(c => c.GetType() == columnizer.GetType()))
+ {
+ _logger.Debug("Columnizer type {Type} already registered, skipping duplicate", columnizer.GetType().FullName);
+ return;
+ }
+
// Add to registered columnizers
RegisteredColumnizers.Add(columnizer);
@@ -890,8 +897,7 @@ private void ProcessLoadedPlugin (object plugin, PluginManifest? manifest, strin
}
}
- // Existing IColumnizerConfigurator support
- if (columnizer is IColumnizerConfigurator configurator)
+ if (columnizer is IColumnizerConfiguratorMemory configurator)
{
try
{
diff --git a/src/PluginRegistry/PluginValidator.cs b/src/PluginRegistry/PluginValidator.cs
index 69ca558f..4d7b72d2 100644
--- a/src/PluginRegistry/PluginValidator.cs
+++ b/src/PluginRegistry/PluginValidator.cs
@@ -40,6 +40,11 @@ public static partial class PluginValidator
// Known safe dependencies (not plugins themselves)
private static readonly HashSet _knownDependencies = new(StringComparer.OrdinalIgnoreCase)
{
+ // LogExpert's own assemblies are not plugins. LogExpert.Core defines the built-in
+ // columnizers; if it (or a copy) lands in the plugins folder it must not be scanned,
+ // or the built-in columnizers would be registered twice.
+ "LogExpert.Core.dll",
+ "LogExpert.Resources.dll",
"ColumnizerLib.dll",
"Newtonsoft.Json.dll",
"CsvHelper.dll",
diff --git a/src/RegexColumnizer/RegexColumnizer.csproj b/src/RegexColumnizer/RegexColumnizer.csproj
index 87ce44ad..6797cf88 100644
--- a/src/RegexColumnizer/RegexColumnizer.csproj
+++ b/src/RegexColumnizer/RegexColumnizer.csproj
@@ -12,7 +12,13 @@
-
+
+
+ false
+