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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 115 additions & 30 deletions src/LogExpert.Configuration/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,8 @@ public Settings Settings
{
get
{
field ??= Load();

return field;
_settings ??= Load();
return _settings;
}
}

Expand Down Expand Up @@ -196,7 +195,7 @@ public ImportResult Import (FileInfo fileInfo, ExportImportFlags importFlags)
// Handle any critical errors from loading
if (loadResult.CriticalFailure)
{
return ImportResult.Failed("Import Failed", $"Import file is invalid or corrupted:\n\n{loadResult.CriticalMessage}\n\nImport cancelled.");
return ImportResult.Failed("Import Failed", $"Import file is invalid or corrupted:\n\n{loadResult.CriticalMessage}\n\nImport canceled.");
}

importedSettings = loadResult.Settings;
Expand All @@ -205,10 +204,10 @@ public ImportResult Import (FileInfo fileInfo, ExportImportFlags importFlags)
JsonSerializationException)
{
_logger.Error($"Import file is invalid or corrupted: {ex}");
return ImportResult.Failed("Import Failed", $"Import file is invalid or corrupted:\n\n{ex.Message}\n\nImport cancelled.");
return ImportResult.Failed("Import Failed", $"Import file is invalid or corrupted:\n\n{ex.Message}\n\nImport canceled.");
}

if (SettingsAreEmptyOrDefault(importedSettings))
if (SettingsAreEmptyOrDefault(importedSettings, importFlags))
{
_logger.Warn("Import file appears to contain empty or default settings");

Expand All @@ -228,8 +227,8 @@ public ImportResult Import (FileInfo fileInfo, ExportImportFlags importFlags)
$"History={importedSettings.FileHistoryList?.Count ?? 0}, " +
$"Highlights={importedSettings.Preferences?.HighlightGroupList?.Count ?? 0}");

// Proceed with import
Instance._settings = Instance.Import(Instance._settings, fileInfo, importFlags);
// Proceed with import - Use Settings property to ensure _settings is initialized
_settings = Instance.Import(Instance.Settings, fileInfo, importFlags);
Save(SettingsFlags.All);

_logger.Info("Import completed successfully");
Expand Down Expand Up @@ -502,6 +501,7 @@ UnauthorizedAccessException or
/// <summary>
/// Initialize settings with required default values
/// </summary>

private static Settings InitializeSettings (Settings settings)
{
settings.Preferences ??= new Preferences();
Expand Down Expand Up @@ -841,20 +841,43 @@ private Settings Import (Settings currentSettings, FileInfo fileInfo, ExportImpo
Settings ownSettings = ObjectClone.Clone(currentSettings);
Settings newSettings;

// at first check for 'Other' as this are the most options.
// Check for 'All' flag first - import everything
if (flags.HasFlag(ExportImportFlags.All))
{
// For All, start with imported settings and selectively keep some current data if KeepExisting is set
newSettings = ObjectClone.Clone(importSettings);

if (flags.HasFlag(ExportImportFlags.KeepExisting))
{
// Merge with existing settings
newSettings.FilterList = ReplaceOrKeepExisting(flags, ownSettings.FilterList, importSettings.FilterList);
newSettings.FileHistoryList = ReplaceOrKeepExisting(flags, ownSettings.FileHistoryList, importSettings.FileHistoryList);
newSettings.SearchHistoryList = ReplaceOrKeepExisting(flags, ownSettings.SearchHistoryList, importSettings.SearchHistoryList);
newSettings.FilterHistoryList = ReplaceOrKeepExisting(flags, ownSettings.FilterHistoryList, importSettings.FilterHistoryList);
newSettings.FilterRangeHistoryList = ReplaceOrKeepExisting(flags, ownSettings.FilterRangeHistoryList, importSettings.FilterRangeHistoryList);

newSettings.Preferences.HighlightGroupList = ReplaceOrKeepExisting(flags, ownSettings.Preferences.HighlightGroupList, importSettings.Preferences.HighlightGroupList);
newSettings.Preferences.ColumnizerMaskList = ReplaceOrKeepExisting(flags, ownSettings.Preferences.ColumnizerMaskList, importSettings.Preferences.ColumnizerMaskList);
newSettings.Preferences.HighlightMaskList = ReplaceOrKeepExisting(flags, ownSettings.Preferences.HighlightMaskList, importSettings.Preferences.HighlightMaskList);
newSettings.Preferences.ToolEntries = ReplaceOrKeepExisting(flags, ownSettings.Preferences.ToolEntries, importSettings.Preferences.ToolEntries);
}

return newSettings;
}

// For partial imports, start with current settings and selectively update
newSettings = ownSettings;

// Check for 'Other' as this covers most preference options
if ((flags & ExportImportFlags.Other) == ExportImportFlags.Other)
{
newSettings = ownSettings;
newSettings.Preferences = ObjectClone.Clone(importSettings.Preferences);
// Preserve specific lists that have their own flags
newSettings.Preferences.ColumnizerMaskList = ownSettings.Preferences.ColumnizerMaskList;
newSettings.Preferences.HighlightMaskList = ownSettings.Preferences.HighlightMaskList;
newSettings.Preferences.HighlightGroupList = ownSettings.Preferences.HighlightGroupList;
newSettings.Preferences.ToolEntries = ownSettings.Preferences.ToolEntries;
}
else
{
newSettings = ownSettings;
}

if ((flags & ExportImportFlags.ColumnizerMasks) == ExportImportFlags.ColumnizerMasks)
{
Expand Down Expand Up @@ -909,12 +932,14 @@ private static void SetBoundsWithinVirtualScreen (Settings settings)
}

/// <summary>
/// Checks if settings object appears to be empty or default.
/// This helps detect corrupted or uninitialized settings files.
/// Checks if settings object appears to be empty or default, considering the import flags.
/// For full imports, all sections are checked. For partial imports, only relevant sections are validated.
/// This helps detect corrupted files while allowing legitimate partial imports.
/// </summary>
/// <param name="settings">Settings object to validate</param>
/// <returns>True if settings appear empty/default, false if they contain user data</returns>
private static bool SettingsAreEmptyOrDefault (Settings settings)
/// <param name="importFlags">Flags indicating which sections are being imported</param>
/// <returns>True if the relevant settings sections appear empty/default, false if they contain user data</returns>
private static bool SettingsAreEmptyOrDefault (Settings settings, ExportImportFlags importFlags)
{
if (settings == null)
{
Expand All @@ -926,17 +951,76 @@ private static bool SettingsAreEmptyOrDefault (Settings settings)
return true;
}

var filterCount = settings.FilterList?.Count ?? 0;
var historyCount = settings.FileHistoryList?.Count ?? 0;
var searchHistoryCount = settings.SearchHistoryList?.Count ?? 0;
var highlightCount = settings.Preferences.HighlightGroupList?.Count ?? 0;
var columnizerMaskCount = settings.Preferences.ColumnizerMaskList?.Count ?? 0;
// For full imports or when no specific flags are set, check all sections
if (importFlags is ExportImportFlags.All or ExportImportFlags.None)
{
var filterCount = settings.FilterList?.Count ?? 0;
var historyCount = settings.FileHistoryList?.Count ?? 0;
var searchHistoryCount = settings.SearchHistoryList?.Count ?? 0;
var highlightCount = settings.Preferences.HighlightGroupList?.Count ?? 0;
var columnizerMaskCount = settings.Preferences.ColumnizerMaskList?.Count ?? 0;

return filterCount == 0 &&
historyCount == 0 &&
searchHistoryCount == 0 &&
highlightCount == 0 &&
columnizerMaskCount == 0;
}

// For partial imports, check only the sections being imported
// At least one relevant section must have data for the import to be valid
bool hasAnyRelevantData = false;

// Check HighlightSettings flag
if (importFlags.HasFlag(ExportImportFlags.HighlightSettings))
{
var highlightCount = settings.Preferences.HighlightGroupList?.Count ?? 0;
if (highlightCount > 0)
{
hasAnyRelevantData = true;
}
}

// Check ColumnizerMasks flag
if (importFlags.HasFlag(ExportImportFlags.ColumnizerMasks))
{
var columnizerMaskCount = settings.Preferences.ColumnizerMaskList?.Count ?? 0;
if (columnizerMaskCount > 0)
{
hasAnyRelevantData = true;
}
}

// Check HighlightMasks flag
if (importFlags.HasFlag(ExportImportFlags.HighlightMasks))
{
var highlightMaskCount = settings.Preferences.HighlightMaskList?.Count ?? 0;
if (highlightMaskCount > 0)
{
hasAnyRelevantData = true;
}
}

// Check ToolEntries flag
if (importFlags.HasFlag(ExportImportFlags.ToolEntries))
{
var toolEntriesCount = settings.Preferences.ToolEntries?.Count ?? 0;
if (toolEntriesCount > 0)
{
hasAnyRelevantData = true;
}
}

// Check Other flag (preferences/settings that don't fall into specific categories)
if (importFlags.HasFlag(ExportImportFlags.Other))
{
// For 'Other', we consider the settings valid if Preferences object exists
// This covers font settings, colors, and other preference data
hasAnyRelevantData = true;
}

return filterCount == 0 &&
historyCount == 0 &&
searchHistoryCount == 0 &&
highlightCount == 0 &&
columnizerMaskCount == 0;
// Return true (isEmpty) if no relevant data was found in any checked section
return !hasAnyRelevantData;
}

/// <summary>
Expand All @@ -959,11 +1043,12 @@ private bool ValidateSettings (Settings settings)
return false;
}

if (SettingsAreEmptyOrDefault(settings))
// For save operations, always validate all sections (use ExportImportFlags.All)
if (SettingsAreEmptyOrDefault(settings, ExportImportFlags.All))
{
_logger.Warn("Settings appear to be empty - this may indicate data loss");

if (_settings != null && !SettingsAreEmptyOrDefault(_settings))
if (_settings != null && !SettingsAreEmptyOrDefault(_settings, ExportImportFlags.All))
{
_logger.Warn($"Previous settings: " +
$"Filters={_settings.FilterList?.Count ?? 0}, " +
Expand Down
19 changes: 11 additions & 8 deletions src/LogExpert.Core/Classes/ObjectClone.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
using System.IO;
using System.Text.Json;
using Newtonsoft.Json;

namespace LogExpert.Core.Classes;

public static class ObjectClone
{
#region Public methods

public static T Clone<T>(T RealObject)
/// <summary>
/// Creates a deep clone of an object using JSON serialization.
/// Uses Newtonsoft.Json to ensure proper handling of complex types like System.Drawing.Color.
/// </summary>
/// <typeparam name="T">Type of object to clone</typeparam>
/// <param name="realObject">Object to clone</param>
/// <returns>Deep clone of the object</returns>
public static T Clone<T> (T realObject)
{
using MemoryStream objectStream = new();

JsonSerializer.Serialize(objectStream, RealObject);
objectStream.Seek(0, SeekOrigin.Begin);
return JsonSerializer.Deserialize<T>(objectStream);
var json = JsonConvert.SerializeObject(realObject);
return JsonConvert.DeserializeObject<T>(json);
}

#endregion
Expand Down
9 changes: 9 additions & 0 deletions src/LogExpert.Core/Config/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ public class Preferences

public bool PortableMode { get; set; }

/// <summary>
/// OBSOLETE: This setting is no longer used. It was originally intended to show an error dialog when "Allow Only One Instance" was enabled,
/// but this behavior was incorrect (showed dialog on success instead of failure). The feature now works silently on success and only shows
/// a warning on IPC failure. This property is kept for backward compatibility with old settings files but is no longer used or saved.
/// Will be removed in a future version.
/// </summary>
[Obsolete("This setting is no longer used and will be removed in a future version. The 'Allow Only One Instance' feature now works silently.")]
[System.Text.Json.Serialization.JsonIgnore]
[Newtonsoft.Json.JsonIgnore]
public bool ShowErrorMessageAllowOnlyOneInstances { get; set; }

/// <summary>
Expand Down
7 changes: 7 additions & 0 deletions src/LogExpert.Core/Interface/ILogExpertProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public interface ILogExpertProxy
/// <param name="logWin"></param>
void WindowClosed(ILogTabWindow logWin);

/// <summary>
/// Notifies the proxy that a window has been activated by the user.
/// Used to track which window should receive new files when "Allow Only One Instance" is enabled.
/// </summary>
/// <param name="window">The window that was activated</param>
void NotifyWindowActivated(ILogTabWindow window);

int GetLogWindowCount();

#endregion
Expand Down
Loading