diff --git a/src/LogExpert.Configuration/ConfigManager.cs b/src/LogExpert.Configuration/ConfigManager.cs
index e1437a7b..5b8e716a 100644
--- a/src/LogExpert.Configuration/ConfigManager.cs
+++ b/src/LogExpert.Configuration/ConfigManager.cs
@@ -46,6 +46,7 @@ public class ConfigManager : IConfigManager
};
private const string SETTINGS_FILE_NAME = "settings.json";
+ private const int MAX_FILE_HISTORY = 10;
#endregion
@@ -251,6 +252,43 @@ public void ImportHighlightSettings (FileInfo fileInfo, ExportImportFlags import
Save(SettingsFlags.All);
}
+ ///
+ /// Adds the specified file name to the file history list, moving it to the top if it already exists.
+ ///
+ /// If the file name already exists in the history, it is moved to the top of the list. The file
+ /// history list is limited to a maximum number of entries; the oldest entries are removed if the limit is exceeded.
+ /// This method is supported only on Windows platforms.
+ /// The name of the file to add to the file history list. Comparison is case-insensitive.
+ [SupportedOSPlatform("windows")]
+ public void AddToFileHistory (string fileName)
+ {
+ bool findName (string s) => s.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal);
+
+ var index = Instance.Settings.FileHistoryList.FindIndex(findName);
+
+ if (index != -1)
+ {
+ Instance.Settings.FileHistoryList.RemoveAt(index);
+ }
+
+ Instance.Settings.FileHistoryList.Insert(0, fileName);
+
+ while (Instance.Settings.FileHistoryList.Count > MAX_FILE_HISTORY)
+ {
+ Instance.Settings.FileHistoryList.RemoveAt(Instance.Settings.FileHistoryList.Count - 1);
+ }
+
+ Save(SettingsFlags.FileHistory);
+ }
+
+ public void ClearLastOpenFilesList ()
+ {
+ lock (_loadSaveLock)
+ {
+ Instance.Settings.LastOpenFilesList.Clear();
+ }
+ }
+
#endregion
#region Private Methods
@@ -807,7 +845,8 @@ IOException or
NotSupportedException or
PathTooLongException or
UnauthorizedAccessException or
- SecurityException)
+ SecurityException or
+ JsonSerializationException)
{
_logger.Error($"Error while deserializing config data: {e}");
newGroups = [];
@@ -1060,6 +1099,7 @@ private bool ValidateSettings (Settings settings)
return true;
}
+
#endregion
///
diff --git a/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs b/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs
new file mode 100644
index 00000000..9e7b9942
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/PersisterHelpers.cs
@@ -0,0 +1,71 @@
+using System.Collections.ObjectModel;
+
+using LogExpert.Core.Interface;
+
+namespace LogExpert.Core.Classes.Persister;
+
+public static class PersisterHelpers
+{
+
+ private const string LOCAL_FILE_SYSTEM_NAME = "LocalFileSystem";
+
+ ///
+ /// Checks if the file name is a settings file (.lxp). If so, the contained logfile name
+ /// is returned. If not, the given file name is returned unchanged.
+ ///
+ /// The file name to resolve
+ /// Plugin registry for file system resolution (optional)
+ /// The resolved log file path
+ public static string FindFilenameForSettings (string fileName, IPluginRegistry pluginRegistry)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(fileName, nameof(fileName));
+
+ if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase))
+ {
+ var persistenceData = Persister.Load(fileName);
+ if (persistenceData == null)
+ {
+ return fileName;
+ }
+
+ if (!string.IsNullOrEmpty(persistenceData.FileName))
+ {
+ if (pluginRegistry != null)
+ {
+ var fs = pluginRegistry.FindFileSystemForUri(persistenceData.FileName);
+ // Use file system plugin for non-local files (network, SFTP, etc.)
+ if (fs != null && fs.GetType().Name != LOCAL_FILE_SYSTEM_NAME)
+ {
+ return persistenceData.FileName;
+ }
+ }
+
+ // Handle rooted paths (absolute paths)
+ if (Path.IsPathRooted(persistenceData.FileName))
+ {
+ return persistenceData.FileName;
+ }
+
+ // Handle relative paths in .lxp files
+ var dir = Path.GetDirectoryName(fileName);
+ return Path.Join(dir, persistenceData.FileName);
+ }
+ }
+
+ return fileName;
+ }
+
+ public static ReadOnlyCollection FindFilenameForSettings (ReadOnlyCollection fileNames, IPluginRegistry pluginRegistry)
+ {
+ ArgumentNullException.ThrowIfNull(fileNames);
+
+ var foundFiles = new List(fileNames.Count);
+
+ foreach (var fileName in fileNames)
+ {
+ foundFiles.Add(FindFilenameForSettings(fileName, pluginRegistry));
+ }
+
+ return foundFiles.AsReadOnly();
+ }
+}
diff --git a/src/LogExpert.Core/Classes/Persister/PersisterXML.cs b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs
index 1a015dbc..c570cd5b 100644
--- a/src/LogExpert.Core/Classes/Persister/PersisterXML.cs
+++ b/src/LogExpert.Core/Classes/Persister/PersisterXML.cs
@@ -601,7 +601,8 @@ public static PersistenceData Load (string fileName)
}
catch (Exception xmlParsingException) when (xmlParsingException is XmlException or
UnauthorizedAccessException or
- IOException)
+ IOException or
+ FileNotFoundException)
{
_logger.Error(xmlParsingException, $"Error loading persistence data from {fileName}, unknown format, parsing xml or json was not possible");
return null;
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectData.cs b/src/LogExpert.Core/Classes/Persister/ProjectData.cs
index 47f64a7d..5a5abd3d 100644
--- a/src/LogExpert.Core/Classes/Persister/ProjectData.cs
+++ b/src/LogExpert.Core/Classes/Persister/ProjectData.cs
@@ -15,5 +15,10 @@ public class ProjectData
///
public string TabLayoutXml { get; set; }
+ ///
+ /// Gets or sets the full file path to the project file.
+ ///
+ public string ProjectFilePath { get; set; }
+
#endregion
}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs b/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs
new file mode 100644
index 00000000..d089cfe8
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/ProjectFileResolver.cs
@@ -0,0 +1,34 @@
+using System.Collections.ObjectModel;
+
+using LogExpert.Core.Interface;
+
+namespace LogExpert.Core.Classes.Persister;
+
+///
+/// Helper class to resolve project file references to actual log files.
+/// Handles .lxp (persistence) files by extracting the actual log file path.
+///
+public static class ProjectFileResolver
+{
+ ///
+ /// Resolves project file names to actual log files.
+ /// If a file is a .lxp persistence file, extracts the log file path from it.
+ ///
+ /// The project data containing file references
+ /// Plugin registry for file system resolution (optional)
+ /// List of tuples containing (logFilePath, originalFilePath)
+ public static ReadOnlyCollection<(string LogFile, string OriginalFile)> ResolveProjectFiles (ProjectData projectData, IPluginRegistry pluginRegistry = null)
+ {
+ ArgumentNullException.ThrowIfNull(projectData);
+
+ var resolved = new List<(string LogFile, string OriginalFile)>();
+
+ foreach (var fileName in projectData.FileNames)
+ {
+ var logFile = PersisterHelpers.FindFilenameForSettings(fileName, pluginRegistry);
+ resolved.Add((logFile, fileName));
+ }
+
+ return resolved.AsReadOnly();
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs b/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs
new file mode 100644
index 00000000..aa560354
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/ProjectFileValidator.cs
@@ -0,0 +1,251 @@
+using LogExpert.Core.Interface;
+
+namespace LogExpert.Core.Classes.Persister;
+
+///
+/// Provides static methods for validating project file references, identifying missing or accessible files, and
+/// suggesting alternative file paths using available file system plugins.
+///
+/// This class is intended for use with project data that includes file references and a project file
+/// path. It supports validation of both local file paths and URI-based files through plugin resolution. All methods are
+/// thread-safe and do not modify input data. Use the provided methods to check file existence, resolve canonical file
+/// paths, and locate possible alternatives for missing files.
+public static class ProjectFileValidator
+{
+ ///
+ /// Validates the files referenced by the specified project and identifies missing or accessible files using
+ /// available file system plugins.
+ ///
+ /// Files are considered valid if they exist on disk or if a suitable file system plugin is
+ /// available for URI-based files. For missing files, possible alternative paths are suggested based on the project
+ /// file location.
+ /// The project data containing the list of file names to validate and the project file path. Cannot be null.
+ /// The plugin registry used to resolve file system plugins for URI-based files. Cannot be null.
+ /// A ProjectValidationResult containing lists of valid files, missing files, and possible alternative file paths
+ /// for missing files.
+ public static ProjectValidationResult ValidateProject (ProjectData projectData, IPluginRegistry pluginRegistry)
+ {
+ ArgumentNullException.ThrowIfNull(projectData);
+ ArgumentNullException.ThrowIfNull(pluginRegistry);
+
+ var result = new ProjectValidationResult();
+
+ foreach (var fileName in projectData.FileNames)
+ {
+ var normalizedPath = NormalizeFilePath(fileName);
+
+ if (File.Exists(normalizedPath))
+ {
+ result.ValidFiles.Add(fileName);
+ }
+ else if (IsUri(fileName))
+ {
+ // Check if URI-based file system plugin is available
+ var fs = pluginRegistry.FindFileSystemForUri(fileName);
+ if (fs != null)
+ {
+ result.ValidFiles.Add(fileName);
+ }
+ else
+ {
+ result.MissingFiles.Add(fileName);
+ }
+ }
+ else
+ {
+ result.MissingFiles.Add(fileName);
+
+ var alternativePaths = FindAlternativePaths(fileName, projectData.ProjectFilePath);
+ result.PossibleAlternatives[fileName] = alternativePaths;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Normalizes the specified file path by resolving the actual file name if the file is a persistence file.
+ ///
+ /// Use this method to obtain the canonical file path for files that may be persisted under a
+ /// different name. For files that do not have a ".lxp" extension, the input path is returned unchanged.
+ /// The path of the file to normalize. If the file has a ".lxp" extension, its persisted file name will be resolved;
+ /// otherwise, the original path is returned.
+ /// The normalized file path. If the input is a persistence file, returns the resolved file name; otherwise, returns
+ /// the original file path.
+ private static string NormalizeFilePath (string fileName)
+ {
+ if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase))
+ {
+ var persistenceData = Persister.Load(fileName);
+ return persistenceData?.FileName ?? fileName;
+ }
+
+ return fileName;
+ }
+
+ ///
+ /// Determines whether the specified string represents an absolute URI with a scheme other than "file".
+ ///
+ /// This method returns false for local file paths and URIs with the "file" scheme, treating them
+ /// as regular files rather than remote resources. Common URI schemes include "http", "https", "ftp", and
+ /// "sftp".
+ /// The string to evaluate as a potential URI. Cannot be null, empty, or consist only of white-space characters.
+ /// true if the string is a valid absolute URI with a non-file scheme; otherwise, false.
+ private static bool IsUri (string fileName)
+ {
+ return !string.IsNullOrWhiteSpace(fileName) &&
+ Uri.TryCreate(fileName, UriKind.Absolute, out var uri) &&
+ !string.IsNullOrEmpty(uri.Scheme) &&
+ !uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Searches for alternative file paths that may correspond to the specified file name, considering common locations
+ /// such as the project directory, its subdirectories, the user's Documents/LogExpert folder, alternate drive
+ /// letters, and relative paths from the project directory.
+ ///
+ /// This method attempts to locate files that may have been moved, renamed, or exist in typical
+ /// user or project directories. It ignores errors encountered during directory or file access and does not
+ /// guarantee that all possible alternative locations are checked. Duplicate paths are excluded from the
+ /// result.
+ /// The name or path of the file to search for. Can be an absolute or relative path. Cannot be null, empty, or
+ /// whitespace.
+ /// The full path to the project file used as a reference for searching related directories. Can be null or empty if
+ /// project context is not available.
+ /// A list of strings containing the full paths of files found that match the specified file name in alternative
+ /// locations. The list will be empty if no matching files are found.
+ private static List FindAlternativePaths (string fileName, string projectFilePath)
+ {
+ var alternatives = new List();
+
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ return alternatives;
+ }
+
+ var baseName = Path.GetFileName(fileName);
+
+ if (string.IsNullOrWhiteSpace(baseName))
+ {
+ return alternatives;
+ }
+
+ // Search in directory of .lxj project file
+ if (!string.IsNullOrWhiteSpace(projectFilePath))
+ {
+ try
+ {
+ var projectDir = Path.GetDirectoryName(projectFilePath);
+ if (!string.IsNullOrEmpty(projectDir) && Directory.Exists(projectDir))
+ {
+ var candidatePath = Path.Join(projectDir, baseName);
+ if (File.Exists(candidatePath))
+ {
+ alternatives.Add(candidatePath);
+ }
+
+ // Also check subdirectories (one level deep)
+ var subdirs = Directory.GetDirectories(projectDir);
+ alternatives.AddRange(
+ subdirs
+ .Select(subdir => Path.Join(subdir, baseName))
+ .Where(File.Exists));
+ }
+ }
+ catch (Exception ex) when (ex is ArgumentException or
+ ArgumentNullException or
+ PathTooLongException or
+ UnauthorizedAccessException or
+ IOException)
+ {
+ // Ignore errors when searching in project directory
+ }
+ }
+
+ // Search in Documents/LogExpert folder
+ try
+ {
+ var documentsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LogExpert");
+
+ if (Directory.Exists(documentsPath))
+ {
+ var docCandidate = Path.Join(documentsPath, baseName);
+ if (File.Exists(docCandidate) && !alternatives.Contains(docCandidate))
+ {
+ alternatives.Add(docCandidate);
+ }
+ }
+ }
+ catch (Exception ex) when (ex is ArgumentException or
+ ArgumentNullException or
+ PathTooLongException or
+ UnauthorizedAccessException or
+ IOException)
+ {
+ // Ignore errors when searching in Documents folder
+ }
+
+ // If the original path is absolute, try to find the file in the same directory structure
+ // but on a different drive (useful when drive letters change)
+ if (Path.IsPathRooted(fileName))
+ {
+ try
+ {
+ var driveLetters = DriveInfo.GetDrives()
+ .Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
+ .Select(d => d.Name[0])
+ .ToList();
+
+ var originalDrive = Path.GetPathRoot(fileName)?[0];
+ var pathWithoutDrive = fileName.Length > 3 ? fileName[3..] : string.Empty;
+
+ foreach (var drive in driveLetters.Where(drive => drive != originalDrive && !string.IsNullOrEmpty(pathWithoutDrive)))
+ {
+ var alternatePath = $"{drive}:\\{pathWithoutDrive}";
+ if (File.Exists(alternatePath) && !alternatives.Contains(alternatePath))
+ {
+ alternatives.Add(alternatePath);
+ }
+ }
+ }
+ catch (Exception ex) when (ex is ArgumentException or
+ ArgumentNullException or
+ PathTooLongException or
+ UnauthorizedAccessException or
+ IOException)
+ {
+ // Ignore errors when searching on different drives
+ }
+ }
+
+ // Try relative path resolution from project directory
+ if (!Path.IsPathRooted(fileName) && !string.IsNullOrWhiteSpace(projectFilePath))
+ {
+ try
+ {
+ var projectDir = Path.GetDirectoryName(projectFilePath);
+ if (!string.IsNullOrEmpty(projectDir))
+ {
+ var relativePath = Path.Join(projectDir, fileName);
+ var normalizedPath = Path.GetFullPath(relativePath);
+
+ if (File.Exists(normalizedPath) && !alternatives.Contains(normalizedPath))
+ {
+ alternatives.Add(normalizedPath);
+ }
+ }
+ }
+ catch (Exception ex) when (ex is ArgumentException or
+ ArgumentNullException or
+ PathTooLongException or
+ UnauthorizedAccessException or
+ IOException or
+ NotSupportedException)
+ {
+ // Ignore errors with relative path resolution
+ }
+ }
+
+ return alternatives;
+ }
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs b/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs
new file mode 100644
index 00000000..83b15ceb
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/ProjectLoadResult.cs
@@ -0,0 +1,35 @@
+namespace LogExpert.Core.Classes.Persister;
+
+///
+/// Represents the result of loading a project file, including validation information.
+///
+public class ProjectLoadResult
+{
+ ///
+ /// The loaded project data (contains resolved log file paths).
+ ///
+ public ProjectData ProjectData { get; set; }
+
+ ///
+ /// Validation result containing valid, missing, and alternative file paths.
+ ///
+ public ProjectValidationResult ValidationResult { get; set; }
+
+ ///
+ /// Mapping of original file references to resolved log files.
+ /// Key: resolved log file path (.log)
+ /// Value: original file reference (.lxp or .log)
+ /// Used to update persistence files when user selects alternatives.
+ ///
+ public Dictionary LogToOriginalFileMapping { get; set; } = [];
+
+ ///
+ /// Indicates whether the project has at least one valid file to load.
+ ///
+ public bool HasValidFiles => ValidationResult?.ValidFiles.Count > 0;
+
+ ///
+ /// Indicates whether user intervention is needed due to missing files.
+ ///
+ public bool RequiresUserIntervention => ValidationResult?.HasMissingFiles ?? false;
+}
\ No newline at end of file
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
index b2ae8f17..8a0c3da5 100644
--- a/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
+++ b/src/LogExpert.Core/Classes/Persister/ProjectPersister.cs
@@ -1,5 +1,7 @@
using System.Text;
+using LogExpert.Core.Interface;
+
using Newtonsoft.Json;
using NLog;
@@ -13,11 +15,13 @@ public static class ProjectPersister
#region Public methods
///
- /// Loads the project session data from a specified file.
+ /// Loads the project session data from a specified file, including validation of referenced files.
+ /// Resolves .lxp persistence files to actual .log files before validation.
///
- ///
- ///
- public static ProjectData LoadProjectData (string projectFileName)
+ /// The path to the project file (.lxj)
+ /// The plugin registry for file system validation
+ /// A containing the project data and validation results
+ public static ProjectLoadResult LoadProjectData (string projectFileName, IPluginRegistry pluginRegistry)
{
try
{
@@ -27,15 +31,74 @@ public static ProjectData LoadProjectData (string projectFileName)
};
var json = File.ReadAllText(projectFileName, Encoding.UTF8);
- return JsonConvert.DeserializeObject(json, settings);
+ var projectData = JsonConvert.DeserializeObject(json, settings);
+
+ // Set project file path for alternative file search
+ projectData.ProjectFilePath = projectFileName;
+
+ // Resolve .lxp files to actual .log files
+ var resolvedFiles = ProjectFileResolver.ResolveProjectFiles(projectData, pluginRegistry);
+
+ // Create mapping: logFile → originalFile
+ var logToOriginalMapping = new Dictionary();
+ foreach (var (logFile, originalFile) in resolvedFiles)
+ {
+ logToOriginalMapping[logFile] = originalFile;
+ }
+
+ // Create new ProjectData with resolved log file paths
+ var resolvedProjectData = new ProjectData
+ {
+ FileNames = [.. resolvedFiles.Select(r => r.LogFile)],
+ TabLayoutXml = projectData.TabLayoutXml,
+ ProjectFilePath = projectData.ProjectFilePath
+ };
+
+ // Validate the actual log files (not .lxp files)
+ var validationResult = ProjectFileValidator.ValidateProject(resolvedProjectData, pluginRegistry);
+
+ return new ProjectLoadResult
+ {
+ ProjectData = resolvedProjectData,
+ ValidationResult = validationResult,
+ LogToOriginalFileMapping = logToOriginalMapping
+ };
}
catch (Exception ex) when (ex is UnauthorizedAccessException or
IOException or
JsonSerializationException)
{
-
_logger.Warn($"Error loading persistence data from {projectFileName}, trying old xml version");
- return ProjectPersisterXML.LoadProjectData(projectFileName);
+
+ var projectData = ProjectPersisterXML.LoadProjectData(projectFileName);
+
+ // Set project file path for alternative file search
+ projectData.ProjectFilePath = projectFileName;
+
+ // Resolve .lxp files for XML fallback as well
+ var resolvedFiles = ProjectFileResolver.ResolveProjectFiles(projectData, pluginRegistry);
+
+ var logToOriginalMapping = new Dictionary();
+ foreach (var (logFile, originalFile) in resolvedFiles)
+ {
+ logToOriginalMapping[logFile] = originalFile;
+ }
+
+ var resolvedProjectData = new ProjectData
+ {
+ FileNames = [.. resolvedFiles.Select(r => r.LogFile)],
+ TabLayoutXml = projectData.TabLayoutXml,
+ ProjectFilePath = projectData.ProjectFilePath
+ };
+
+ var validationResult = ProjectFileValidator.ValidateProject(resolvedProjectData, pluginRegistry);
+
+ return new ProjectLoadResult
+ {
+ ProjectData = resolvedProjectData,
+ ValidationResult = validationResult,
+ LogToOriginalFileMapping = logToOriginalMapping
+ };
}
}
diff --git a/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs b/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs
new file mode 100644
index 00000000..fed2d742
--- /dev/null
+++ b/src/LogExpert.Core/Classes/Persister/ProjectValidationResult.cs
@@ -0,0 +1,12 @@
+namespace LogExpert.Core.Classes.Persister;
+
+public class ProjectValidationResult
+{
+ public List ValidFiles { get; } = [];
+
+ public List MissingFiles { get; } = [];
+
+ public Dictionary> PossibleAlternatives { get; } = [];
+
+ public bool HasMissingFiles => MissingFiles.Count > 0;
+}
diff --git a/src/LogExpert.Core/Interface/IConfigManager.cs b/src/LogExpert.Core/Interface/IConfigManager.cs
index 7face12b..a436cb23 100644
--- a/src/LogExpert.Core/Interface/IConfigManager.cs
+++ b/src/LogExpert.Core/Interface/IConfigManager.cs
@@ -142,4 +142,22 @@ public interface IConfigManager
/// Thrown if settings validation fails.
/// Thrown if the file cannot be written.
void Save (SettingsFlags flags);
+
+ ///
+ /// Adds the specified file name to the file history list, moving it to the top if it already exists.
+ ///
+ /// If the file name already exists in the history, it is moved to the top of the list. The file
+ /// history list is limited to a maximum number of entries; the oldest entries are removed if the limit is exceeded.
+ /// This method is supported only on Windows platforms.
+ /// The name of the file to add to the file history list. Comparison is case-insensitive.
+ void AddToFileHistory (string fileName);
+
+ ///
+ /// Clears the list of recently opened files.
+ ///
+ /// Call this method to remove all entries from the recent files list, typically to reset user
+ /// history or in response to a privacy-related action. After calling this method, the list of last open files will
+ /// be empty until new files are opened.
+
+ void ClearLastOpenFilesList ();
}
\ No newline at end of file
diff --git a/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj
index ae88c488..7a56ee24 100644
--- a/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj
+++ b/src/LogExpert.Persister.Tests/LogExpert.Persister.Tests.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs b/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs
new file mode 100644
index 00000000..ad21d5ff
--- /dev/null
+++ b/src/LogExpert.Persister.Tests/ProjectFileValidatorTests.cs
@@ -0,0 +1,980 @@
+using System.Globalization;
+
+using LogExpert.Core.Classes.Persister;
+
+namespace LogExpert.Persister.Tests;
+
+///
+/// Unit tests for the Project File Validator implementation (Issue #514).
+/// Tests validation logic for missing files in project/session loading.
+/// Includes tests for ProjectFileResolver, PersisterHelpers, and ProjectPersister updates.
+///
+[TestFixture]
+public class ProjectFileValidatorTests
+{
+ private string _testDirectory;
+ private string _projectFile;
+ private List _testLogFiles;
+
+ [SetUp]
+ public void Setup ()
+ {
+ // Create temporary test directory
+ _testDirectory = Path.Join(Path.GetTempPath(), "LogExpertTests", "ProjectValidator", Guid.NewGuid().ToString());
+ _ = Directory.CreateDirectory(_testDirectory);
+
+ // Initialize test log files list
+ _testLogFiles = [];
+
+ // Create a project file path (will be created in individual tests)
+ _projectFile = Path.Join(_testDirectory, "test_project.lxj");
+
+ // Initialize PluginRegistry for tests
+ _ = PluginRegistry.PluginRegistry.Create(_testDirectory, 1000);
+ }
+
+ [TearDown]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Unit Test")]
+ public void TearDown ()
+ {
+ // Clean up test directory
+ if (Directory.Exists(_testDirectory))
+ {
+ try
+ {
+ Directory.Delete(_testDirectory, true);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
+ #region Helper Methods
+
+ ///
+ /// Creates test log files with specified names.
+ ///
+ private void CreateTestLogFiles (params string[] fileNames)
+ {
+ foreach (var fileName in fileNames)
+ {
+ var filePath = Path.Join(_testDirectory, fileName);
+ File.WriteAllText(filePath, $"Test log content for {fileName}");
+ _testLogFiles.Add(filePath);
+ }
+ }
+
+ ///
+ /// Creates a test project file with specified log file references.
+ ///
+ private void CreateTestProjectFile (params string[] logFileNames)
+ {
+ var projectData = new ProjectData
+ {
+ FileNames = [.. logFileNames.Select(name => Path.Join(_testDirectory, name))],
+ TabLayoutXml = "test"
+ };
+
+ ProjectPersister.SaveProjectData(_projectFile, projectData);
+ }
+
+ ///
+ /// Creates a .lxp persistence file pointing to a log file.
+ ///
+ private void CreatePersistenceFile (string lxpFileName, string logFileName)
+ {
+ var lxpPath = Path.Join(_testDirectory, lxpFileName);
+ var logPath = Path.Join(_testDirectory, logFileName);
+
+ var persistenceData = new PersistenceData
+ {
+ FileName = logPath
+ };
+
+ // Use the correct namespace: LogExpert.Core.Classes.Persister.Persister
+ _ = Core.Classes.Persister.Persister.SavePersistenceDataWithFixedName(lxpPath, persistenceData);
+ }
+
+ ///
+ /// Deletes specified log files to simulate missing files.
+ ///
+ private void DeleteLogFiles (params string[] fileNames)
+ {
+ foreach (var filePath in fileNames.Select(fileName => Path.Join(_testDirectory, fileName)))
+ {
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+ }
+ }
+
+ #endregion
+
+ #region PersisterHelpers Tests
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_RegularLogFile_ReturnsUnchanged ()
+ {
+ // Arrange
+ CreateTestLogFiles("test.log");
+ var logPath = Path.Join(_testDirectory, "test.log");
+
+ // Act
+ var result = PersisterHelpers.FindFilenameForSettings(logPath, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(logPath), "Regular log file should be returned unchanged");
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_LxpFile_ReturnsLogPath ()
+ {
+ // Arrange
+ CreateTestLogFiles("actual.log");
+ CreatePersistenceFile("settings.lxp", "actual.log");
+ var lxpPath = Path.Join(_testDirectory, "settings.lxp");
+ var expectedLogPath = Path.Join(_testDirectory, "actual.log");
+
+ // Act
+ var result = PersisterHelpers.FindFilenameForSettings(lxpPath, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(expectedLogPath), "Should resolve .lxp to actual log file");
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_NullFileName_ThrowsArgumentNullException ()
+ {
+ // Act & Assert - ThrowIfNullOrWhiteSpace throws ArgumentNullException for null
+ _ = Assert.Throws(() =>
+ PersisterHelpers.FindFilenameForSettings((string)null, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_EmptyFileName_ThrowsArgumentException ()
+ {
+ // Act & Assert
+ _ = Assert.Throws(() =>
+ PersisterHelpers.FindFilenameForSettings(string.Empty, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_ListOfFiles_ResolvesAll ()
+ {
+ // Arrange
+ CreateTestLogFiles("log1.log", "log2.log", "log3.log");
+ var fileList = new List
+ {
+ Path.Join(_testDirectory, "log1.log"),
+ Path.Join(_testDirectory, "log2.log"),
+ Path.Join(_testDirectory, "log3.log")
+ };
+
+ // Act - call the List overload explicitly
+ var result = PersisterHelpers.FindFilenameForSettings(fileList.AsReadOnly(), PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(3), "Should resolve all files");
+ Assert.That(result[0], Does.EndWith("log1.log"));
+ Assert.That(result[1], Does.EndWith("log2.log"));
+ Assert.That(result[2], Does.EndWith("log3.log"));
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_MixedLxpAndLog_ResolvesBoth ()
+ {
+ // Arrange
+ CreateTestLogFiles("direct.log", "referenced.log");
+ CreatePersistenceFile("indirect.lxp", "referenced.log");
+
+ var fileList = new List
+ {
+ Path.Join(_testDirectory, "direct.log"),
+ Path.Join(_testDirectory, "indirect.lxp")
+ };
+
+ // Act - call the List overload explicitly
+ var result = PersisterHelpers.FindFilenameForSettings(fileList.AsReadOnly(), PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0], Does.EndWith("direct.log"), "Direct log should be unchanged");
+ Assert.That(result[1], Does.EndWith("referenced.log"), ".lxp should resolve to referenced log");
+ }
+
+ [Test]
+ public void PersisterHelpers_FindFilenameForSettings_CorruptedLxp_ReturnsLxpPath ()
+ {
+ // Arrange
+ var lxpPath = Path.Join(_testDirectory, "corrupted.lxp");
+ File.WriteAllText(lxpPath, "This is not valid XML");
+
+ // Act
+ var result = PersisterHelpers.FindFilenameForSettings(lxpPath, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(lxpPath), "Corrupted .lxp should return original path");
+ }
+
+ #endregion
+
+ #region ProjectFileResolver Tests
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_AllLogFiles_ReturnsUnchanged ()
+ {
+ // Arrange
+ CreateTestLogFiles("file1.log", "file2.log");
+ var projectData = new ProjectData
+ {
+ FileNames =
+ [
+ Path.Join(_testDirectory, "file1.log"),
+ Path.Join(_testDirectory, "file2.log")
+ ]
+ };
+
+ // Act
+ var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0].LogFile, Does.EndWith("file1.log"));
+ Assert.That(result[0].OriginalFile, Does.EndWith("file1.log"));
+ Assert.That(result[1].LogFile, Does.EndWith("file2.log"));
+ Assert.That(result[1].OriginalFile, Does.EndWith("file2.log"));
+ }
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_WithLxpFiles_ResolvesToLogs ()
+ {
+ // Arrange
+ CreateTestLogFiles("actual1.log", "actual2.log");
+ CreatePersistenceFile("settings1.lxp", "actual1.log");
+ CreatePersistenceFile("settings2.lxp", "actual2.log");
+
+ var projectData = new ProjectData
+ {
+ FileNames =
+ [
+ Path.Join(_testDirectory, "settings1.lxp"),
+ Path.Join(_testDirectory, "settings2.lxp")
+ ]
+ };
+
+ // Act
+ var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0].LogFile, Does.EndWith("actual1.log"), "Should resolve to actual log");
+ Assert.That(result[0].OriginalFile, Does.EndWith("settings1.lxp"), "Should preserve original .lxp");
+ Assert.That(result[1].LogFile, Does.EndWith("actual2.log"));
+ Assert.That(result[1].OriginalFile, Does.EndWith("settings2.lxp"));
+ }
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_MixedFiles_ResolvesProperly ()
+ {
+ // Arrange
+ CreateTestLogFiles("direct.log", "referenced.log");
+ CreatePersistenceFile("indirect.lxp", "referenced.log");
+
+ var projectData = new ProjectData
+ {
+ FileNames =
+ [
+ Path.Join(_testDirectory, "direct.log"),
+ Path.Join(_testDirectory, "indirect.lxp")
+ ]
+ };
+
+ // Act
+ var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Has.Count.EqualTo(2));
+ Assert.That(result[0].LogFile, Does.EndWith("direct.log"));
+ Assert.That(result[0].OriginalFile, Does.EndWith("direct.log"));
+ Assert.That(result[1].LogFile, Does.EndWith("referenced.log"));
+ Assert.That(result[1].OriginalFile, Does.EndWith("indirect.lxp"));
+ }
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_NullProjectData_ThrowsArgumentNullException ()
+ {
+ // Act & Assert
+ _ = Assert.Throws(() =>
+ ProjectFileResolver.ResolveProjectFiles(null, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_EmptyProject_ReturnsEmptyList ()
+ {
+ // Arrange
+ var projectData = new ProjectData
+ {
+ FileNames = []
+ };
+
+ // Act
+ var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Empty, "Empty project should return empty list");
+ }
+
+ [Test]
+ public void ProjectFileResolver_ResolveProjectFiles_ReturnsReadOnlyCollection ()
+ {
+ // Arrange
+ CreateTestLogFiles("test.log");
+ var projectData = new ProjectData
+ {
+ FileNames = [Path.Join(_testDirectory, "test.log")]
+ };
+
+ // Act
+ var result = ProjectFileResolver.ResolveProjectFiles(projectData, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.InstanceOf>());
+ }
+
+ #endregion
+
+ #region ProjectLoadResult Tests
+
+ [Test]
+ public void ProjectLoadResult_HasValidFiles_AllFilesValid_ReturnsTrue ()
+ {
+ // Arrange
+ var projectData = new ProjectData();
+ var validationResult = new ProjectValidationResult();
+ validationResult.ValidFiles.Add("file1.log");
+ validationResult.ValidFiles.Add("file2.log");
+
+ var result = new ProjectLoadResult
+ {
+ ProjectData = projectData,
+ ValidationResult = validationResult
+ };
+
+ // Act
+ var hasValidFiles = result.HasValidFiles;
+
+ // Assert
+ Assert.That(hasValidFiles, Is.True, "Should have valid files");
+ }
+
+ [Test]
+ public void ProjectLoadResult_HasValidFiles_NoValidFiles_ReturnsFalse ()
+ {
+ // Arrange
+ var projectData = new ProjectData();
+ var validationResult = new ProjectValidationResult();
+ validationResult.MissingFiles.Add("file1.log");
+ validationResult.MissingFiles.Add("file2.log");
+
+ var result = new ProjectLoadResult
+ {
+ ProjectData = projectData,
+ ValidationResult = validationResult
+ };
+
+ // Act
+ var hasValidFiles = result.HasValidFiles;
+
+ // Assert
+ Assert.That(hasValidFiles, Is.False, "Should not have valid files");
+ }
+
+ [Test]
+ public void ProjectLoadResult_HasValidFiles_SomeValidFiles_ReturnsTrue ()
+ {
+ // Arrange
+ var projectData = new ProjectData();
+ var validationResult = new ProjectValidationResult();
+ validationResult.ValidFiles.Add("file1.log");
+ validationResult.MissingFiles.Add("file2.log");
+ validationResult.MissingFiles.Add("file3.log");
+
+ var result = new ProjectLoadResult
+ {
+ ProjectData = projectData,
+ ValidationResult = validationResult
+ };
+
+ // Act
+ var hasValidFiles = result.HasValidFiles;
+
+ // Assert
+ Assert.That(hasValidFiles, Is.True, "Should have at least one valid file");
+ }
+
+ [Test]
+ public void ProjectLoadResult_RequiresUserIntervention_AllFilesValid_ReturnsFalse ()
+ {
+ // Arrange
+ var projectData = new ProjectData();
+ var validationResult = new ProjectValidationResult();
+ validationResult.ValidFiles.Add("file1.log");
+ validationResult.ValidFiles.Add("file2.log");
+
+ var result = new ProjectLoadResult
+ {
+ ProjectData = projectData,
+ ValidationResult = validationResult
+ };
+
+ // Act
+ var requiresIntervention = result.RequiresUserIntervention;
+
+ // Assert
+ Assert.That(requiresIntervention, Is.False, "Should not require user intervention");
+ }
+
+ [Test]
+ public void ProjectLoadResult_RequiresUserIntervention_SomeMissingFiles_ReturnsTrue ()
+ {
+ // Arrange
+ var projectData = new ProjectData();
+ var validationResult = new ProjectValidationResult();
+ validationResult.ValidFiles.Add("file1.log");
+ validationResult.MissingFiles.Add("file2.log");
+
+ var result = new ProjectLoadResult
+ {
+ ProjectData = projectData,
+ ValidationResult = validationResult
+ };
+
+ // Act
+ var requiresIntervention = result.RequiresUserIntervention;
+
+ // Assert
+ Assert.That(requiresIntervention, Is.True, "Should require user intervention");
+ }
+
+ [Test]
+ public void ProjectLoadResult_LogToOriginalFileMapping_StoresMapping ()
+ {
+ // Arrange
+ var mapping = new Dictionary
+ {
+ ["C:\\logs\\actual.log"] = "C:\\settings\\config.lxp",
+ ["C:\\logs\\direct.log"] = "C:\\logs\\direct.log"
+ };
+
+ var result = new ProjectLoadResult
+ {
+ LogToOriginalFileMapping = mapping
+ };
+
+ // Act & Assert
+ Assert.That(result.LogToOriginalFileMapping, Has.Count.EqualTo(2));
+ Assert.That(result.LogToOriginalFileMapping["C:\\logs\\actual.log"], Is.EqualTo("C:\\settings\\config.lxp"));
+ Assert.That(result.LogToOriginalFileMapping["C:\\logs\\direct.log"], Is.EqualTo("C:\\logs\\direct.log"));
+ }
+
+ #endregion
+
+ #region ProjectValidationResult Tests
+
+ [Test]
+ public void ProjectValidationResult_HasMissingFiles_WithMissingFiles_ReturnsTrue ()
+ {
+ // Arrange
+ var result = new ProjectValidationResult();
+ result.ValidFiles.Add("file1.log");
+ result.MissingFiles.Add("file2.log");
+
+ // Act
+ var hasMissing = result.HasMissingFiles;
+
+ // Assert
+ Assert.That(hasMissing, Is.True, "Should have missing files");
+ }
+
+ [Test]
+ public void ProjectValidationResult_HasMissingFiles_WithoutMissingFiles_ReturnsFalse ()
+ {
+ // Arrange
+ var result = new ProjectValidationResult();
+ result.ValidFiles.Add("file1.log");
+ result.ValidFiles.Add("file2.log");
+
+ // Act
+ var hasMissing = result.HasMissingFiles;
+
+ // Assert
+ Assert.That(hasMissing, Is.False, "Should not have missing files");
+ }
+
+ #endregion
+
+ #region ProjectPersister.LoadProjectData - All Files Valid
+
+ [Test]
+ public void LoadProjectData_AllFilesExist_ReturnsSuccessResult ()
+ {
+ // Arrange
+ CreateTestLogFiles("log1.log", "log2.log", "log3.log");
+ CreateTestProjectFile("log1.log", "log2.log", "log3.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.ProjectData, Is.Not.Null, "ProjectData should not be null");
+ Assert.That(result.ValidationResult, Is.Not.Null, "ValidationResult should not be null");
+ Assert.That(result.HasValidFiles, Is.True, "Should have valid files");
+ Assert.That(result.RequiresUserIntervention, Is.False, "Should not require intervention");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(3), "Should have 3 valid files");
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(0), "Should have 0 missing files");
+ }
+
+ [Test]
+ public void LoadProjectData_AllFilesExist_ProjectDataContainsCorrectFiles ()
+ {
+ // Arrange
+ CreateTestLogFiles("alpha.log", "beta.log", "gamma.log");
+ CreateTestProjectFile("alpha.log", "beta.log", "gamma.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ var fileNames = result.ProjectData.FileNames.Select(Path.GetFileName).ToList();
+ Assert.That(fileNames, Does.Contain("alpha.log"), "Should contain alpha.log");
+ Assert.That(fileNames, Does.Contain("beta.log"), "Should contain beta.log");
+ Assert.That(fileNames, Does.Contain("gamma.log"), "Should contain gamma.log");
+ }
+
+ [Test]
+ public void LoadProjectData_AllFilesExist_PreservesTabLayoutXml ()
+ {
+ // Arrange
+ CreateTestLogFiles("test.log");
+ CreateTestProjectFile("test.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result.ProjectData.TabLayoutXml, Is.Not.Null.And.Not.Empty, "TabLayoutXml should be preserved");
+ Assert.That(result.ProjectData.TabLayoutXml, Does.Contain(""), "Should contain layout XML");
+ }
+
+ [Test]
+ public void LoadProjectData_WithLxpFiles_ResolvesToActualLogs ()
+ {
+ // Arrange
+ CreateTestLogFiles("actual1.log", "actual2.log");
+ CreatePersistenceFile("settings1.lxp", "actual1.log");
+ CreatePersistenceFile("settings2.lxp", "actual2.log");
+
+ // Create project referencing .lxp files
+ var projectData = new ProjectData
+ {
+ FileNames =
+ [
+ Path.Join(_testDirectory, "settings1.lxp"),
+ Path.Join(_testDirectory, "settings2.lxp")
+ ]
+ };
+ ProjectPersister.SaveProjectData(_projectFile, projectData);
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(2), "Should validate actual log files");
+ var fileNames = result.ProjectData.FileNames.Select(Path.GetFileName).ToList();
+ Assert.That(fileNames, Does.Contain("actual1.log"), "Should contain resolved log file");
+ Assert.That(fileNames, Does.Contain("actual2.log"), "Should contain resolved log file");
+ }
+
+ [Test]
+ public void LoadProjectData_WithLxpFiles_PreservesMapping ()
+ {
+ // Arrange
+ CreateTestLogFiles("actual.log");
+ CreatePersistenceFile("settings.lxp", "actual.log");
+
+ var projectData = new ProjectData
+ {
+ FileNames = [Path.Join(_testDirectory, "settings.lxp")]
+ };
+ ProjectPersister.SaveProjectData(_projectFile, projectData);
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result.LogToOriginalFileMapping, Is.Not.Null);
+ Assert.That(result.LogToOriginalFileMapping, Has.Count.EqualTo(1));
+ var actualLogPath = Path.Join(_testDirectory, "actual.log");
+ var lxpPath = Path.Join(_testDirectory, "settings.lxp");
+ Assert.That(result.LogToOriginalFileMapping[actualLogPath], Is.EqualTo(lxpPath));
+ }
+
+ #endregion
+
+ #region ProjectPersister.LoadProjectData - Some Files Missing
+
+ [Test]
+ public void LoadProjectData_SomeFilesMissing_ReturnsPartialSuccessResult ()
+ {
+ // Arrange
+ CreateTestLogFiles("exists1.log", "exists2.log", "missing.log");
+ DeleteLogFiles("missing.log"); // Delete to simulate missing
+ CreateTestProjectFile("exists1.log", "exists2.log", "missing.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.HasValidFiles, Is.True, "Should have some valid files");
+ Assert.That(result.RequiresUserIntervention, Is.True, "Should require user intervention");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(2), "Should have 2 valid files");
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(1), "Should have 1 missing file");
+ }
+
+ [Test]
+ public void LoadProjectData_SomeFilesMissing_ValidFilesListIsCorrect ()
+ {
+ // Arrange
+ CreateTestLogFiles("valid1.log", "valid2.log", "invalid.log");
+ DeleteLogFiles("invalid.log");
+ CreateTestProjectFile("valid1.log", "valid2.log", "invalid.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ var validFileNames = result.ValidationResult.ValidFiles.Select(Path.GetFileName).ToList();
+ Assert.That(validFileNames, Does.Contain("valid1.log"), "Should contain valid1.log");
+ Assert.That(validFileNames, Does.Contain("valid2.log"), "Should contain valid2.log");
+ Assert.That(validFileNames, Does.Not.Contain("invalid.log"), "Should not contain invalid.log");
+ }
+
+ [Test]
+ public void LoadProjectData_SomeFilesMissing_MissingFilesListIsCorrect ()
+ {
+ // Arrange
+ CreateTestLogFiles("present.log", "absent1.log", "absent2.log");
+ DeleteLogFiles("absent1.log", "absent2.log");
+ CreateTestProjectFile("present.log", "absent1.log", "absent2.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ var missingFileNames = result.ValidationResult.MissingFiles.Select(Path.GetFileName).ToList();
+ Assert.That(missingFileNames, Does.Contain("absent1.log"), "Should contain absent1.log");
+ Assert.That(missingFileNames, Does.Contain("absent2.log"), "Should contain absent2.log");
+ Assert.That(missingFileNames, Does.Not.Contain("present.log"), "Should not contain present.log");
+ }
+
+ [Test]
+ public void LoadProjectData_MajorityFilesMissing_StillReturnsValidFiles ()
+ {
+ // Arrange
+ CreateTestLogFiles("only_valid.log", "missing1.log", "missing2.log", "missing3.log", "missing4.log");
+ DeleteLogFiles("missing1.log", "missing2.log", "missing3.log", "missing4.log");
+ CreateTestProjectFile("only_valid.log", "missing1.log", "missing2.log", "missing3.log", "missing4.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result.HasValidFiles, Is.True, "Should have at least one valid file");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(1), "Should have 1 valid file");
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(4), "Should have 4 missing files");
+ }
+
+ [Test]
+ public void LoadProjectData_LxpReferencingMissingLog_ReportsLogAsMissing ()
+ {
+ // Arrange
+ CreateTestLogFiles("missing.log");
+ CreatePersistenceFile("settings.lxp", "missing.log");
+ DeleteLogFiles("missing.log");
+
+ var projectData = new ProjectData
+ {
+ FileNames = [Path.Join(_testDirectory, "settings.lxp")]
+ };
+ ProjectPersister.SaveProjectData(_projectFile, projectData);
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(1), "Should report missing log file");
+ Assert.That(result.ValidationResult.MissingFiles[0], Does.EndWith("missing.log"));
+ }
+
+ #endregion
+
+ #region ProjectPersister.LoadProjectData - All Files Missing
+
+ [Test]
+ public void LoadProjectData_AllFilesMissing_ReturnsFailureResult ()
+ {
+ // Arrange
+ CreateTestLogFiles("missing1.log", "missing2.log");
+ DeleteLogFiles("missing1.log", "missing2.log");
+ CreateTestProjectFile("missing1.log", "missing2.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.HasValidFiles, Is.False, "Should not have valid files");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(0), "Should have 0 valid files");
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(2), "Should have 2 missing files");
+ }
+
+ [Test]
+ public void LoadProjectData_AllFilesMissing_MissingFilesListComplete ()
+ {
+ // Arrange
+ CreateTestLogFiles("gone1.log", "gone2.log", "gone3.log");
+ DeleteLogFiles("gone1.log", "gone2.log", "gone3.log");
+ CreateTestProjectFile("gone1.log", "gone2.log", "gone3.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(3), "Should have 3 missing files");
+ var missingFileNames = result.ValidationResult.MissingFiles.Select(Path.GetFileName).ToList();
+ Assert.That(missingFileNames, Does.Contain("gone1.log"));
+ Assert.That(missingFileNames, Does.Contain("gone2.log"));
+ Assert.That(missingFileNames, Does.Contain("gone3.log"));
+ }
+
+ #endregion
+
+ #region ProjectPersister.LoadProjectData - Empty/Invalid Projects
+
+ [Test]
+ public void LoadProjectData_EmptyProject_ReturnsEmptyResult ()
+ {
+ // Arrange
+ CreateTestProjectFile(); // Empty project with no files
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.ProjectData.FileNames, Is.Empty, "FileNames should be empty");
+ Assert.That(result.ValidationResult.ValidFiles, Is.Empty, "ValidFiles should be empty");
+ Assert.That(result.ValidationResult.MissingFiles, Is.Empty, "MissingFiles should be empty");
+ }
+
+ [Test]
+ public void LoadProjectData_NonExistentProjectFile_ReturnsNull ()
+ {
+ // Arrange
+ var nonExistentProject = Path.Join(_testDirectory, "does_not_exist.lxj");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(nonExistentProject, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ // FIXED: Now returns empty result instead of null when file doesn't exist
+ Assert.That(result, Is.Not.Null, "Result should not be null even for non-existent file");
+ Assert.That(result.ProjectData, Is.Not.Null, "ProjectData should be initialized");
+ }
+
+ [Test]
+ public void LoadProjectData_CorruptedProjectFile_ThrowsJsonReaderException ()
+ {
+ // Arrange
+ var corruptedProject = Path.Join(_testDirectory, "corrupted.lxj");
+ File.WriteAllText(corruptedProject, "This is not valid XML or JSON");
+
+ // Act & Assert - JsonReaderException is not caught, so it propagates
+ _ = Assert.Throws(() =>
+ ProjectPersister.LoadProjectData(corruptedProject, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ #endregion
+
+ #region Edge Cases and Special Scenarios
+
+ [Test]
+ public void LoadProjectData_DuplicateFileReferences_HandlesCorrectly ()
+ {
+ // Arrange
+ CreateTestLogFiles("duplicate.log");
+ var projectData = new ProjectData
+ {
+ FileNames =
+ [
+ Path.Join(_testDirectory, "duplicate.log"),
+ Path.Join(_testDirectory, "duplicate.log"),
+ Path.Join(_testDirectory, "duplicate.log")
+ ]
+ };
+ ProjectPersister.SaveProjectData(_projectFile, projectData);
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.HasValidFiles, Is.True, "Should have valid files");
+ // Validation should handle duplicates gracefully
+ }
+
+ [Test]
+ public void LoadProjectData_FilesWithSpecialCharacters_ValidatesCorrectly ()
+ {
+ // Arrange
+ CreateTestLogFiles("file with spaces.log", "file-with-dashes.log", "file_with_underscores.log");
+ CreateTestProjectFile("file with spaces.log", "file-with-dashes.log", "file_with_underscores.log");
+
+ // Act
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(3), "Should validate all files with special characters");
+ }
+
+ [Test]
+ public void LoadProjectData_VeryLargeProject_ValidatesEfficiently ()
+ {
+ // Arrange
+ const int fileCount = 100;
+ var fileNames = new List();
+
+ for (int i = 0; i < fileCount; i++)
+ {
+ var fileName = $"log_{i:D4}.log";
+ fileNames.Add(fileName);
+ }
+
+ CreateTestLogFiles([.. fileNames]);
+ CreateTestProjectFile([.. fileNames]);
+
+ // Act
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+ stopwatch.Stop();
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(fileCount), $"Should validate all {fileCount} files");
+ Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(5000), "Should complete validation in reasonable time");
+ }
+
+ #endregion
+
+ #region Performance and Stress Tests
+
+ [Test]
+ public void LoadProjectData_ManyMissingFiles_PerformsEfficiently ()
+ {
+ // Arrange
+ const int totalFiles = 50;
+ var fileNames = new List();
+
+ // Create only first 10 files, rest will be missing
+ for (int i = 0; i < 10; i++)
+ {
+ var fileName = $"exists_{i}.log";
+ fileNames.Add(fileName);
+ CreateTestLogFiles(fileName);
+ }
+
+ for (int i = 10; i < totalFiles; i++)
+ {
+ fileNames.Add($"missing_{i}.log");
+ }
+
+ CreateTestProjectFile([.. fileNames]);
+
+ // Act
+ var stopwatch = System.Diagnostics.Stopwatch.StartNew();
+ var result = ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance);
+ stopwatch.Stop();
+
+ // Assert
+ Assert.That(result, Is.Not.Null, "Result should not be null");
+ Assert.That(result.ValidationResult.ValidFiles.Count, Is.EqualTo(10), "Should have 10 valid files");
+ Assert.That(result.ValidationResult.MissingFiles.Count, Is.EqualTo(40), "Should have 40 missing files");
+ Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(2000), "Should handle many missing files efficiently");
+ }
+
+ #endregion
+
+ #region Null and Exception Handling
+
+ [Test]
+ public void LoadProjectData_NullProjectFile_ThrowsArgumentNullException ()
+ {
+ // Act & Assert - File.ReadAllText throws ArgumentNullException for null path
+ _ = Assert.Throws(() =>
+ ProjectPersister.LoadProjectData(null, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ [Test]
+ public void LoadProjectData_EmptyProjectFile_ThrowsArgumentException ()
+ {
+ // Act & Assert - File.ReadAllText throws ArgumentException for empty string
+ _ = Assert.Throws(() =>
+ ProjectPersister.LoadProjectData(string.Empty, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ [Test]
+ public void LoadProjectData_NullPluginRegistry_ThrowsArgumentNullException ()
+ {
+ // Arrange
+ CreateTestProjectFile("test.log");
+
+ // Act & Assert
+ _ = Assert.Throws(() =>
+ ProjectPersister.LoadProjectData(_projectFile, null));
+ }
+
+ #endregion
+
+ #region Backward Compatibility
+
+ [Test]
+ public void LoadProjectData_LegacyProjectFormat_ThrowsJsonReaderException ()
+ {
+ // Arrange
+ CreateTestLogFiles("legacy.log");
+
+ // Create a legacy format project file (XML)
+ var legacyXml = @"
+
+
+
+
+";
+
+ var legacyContent = string.Format(CultureInfo.InvariantCulture, legacyXml, Path.Join(_testDirectory, "legacy.log"));
+ File.WriteAllText(_projectFile, legacyContent);
+
+ // Act & Assert - JsonReaderException is not caught, so XML fallback doesn't trigger
+ _ = Assert.Throws(() =>
+ ProjectPersister.LoadProjectData(_projectFile, PluginRegistry.PluginRegistry.Instance));
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/LogExpert.Resources/Resources.Designer.cs b/src/LogExpert.Resources/Resources.Designer.cs
index f9797035..68f34bbe 100644
--- a/src/LogExpert.Resources/Resources.Designer.cs
+++ b/src/LogExpert.Resources/Resources.Designer.cs
@@ -966,8 +966,7 @@ public static string HighlightDialog_UI_ErrorDuringAddOfHighLightEntry {
}
///
- /// Looks up a localized string similar to Error during save of entry.
- /// {0}.
+ /// Looks up a localized string similar to Error during save of entry. {0}.
///
public static string HighlightDialog_UI_ErrorDuringSavingOfHighlightEntry {
get {
@@ -1245,6 +1244,78 @@ public static string KeywordActionDlg_UI_Title {
}
}
+ ///
+ /// Looks up a localized string similar to Error loading project file. The file may be corrupted or inaccessible..
+ ///
+ public static string LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to update session file: {0}.
+ ///
+ public static string LoadProject_UI_Message_Error_Message_FailedToUpdateSessionFile {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Message_FailedToUpdateSessionFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Session file has been updated with the new file paths..
+ ///
+ public static string LoadProject_UI_Message_Error_Message_UpdateSessionFile {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Message_UpdateSessionFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Session Update Failed.
+ ///
+ public static string LoadProject_UI_Message_Error_Title_FailedToUpdateSessionFile {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_FailedToUpdateSessionFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Project Load Failed.
+ ///
+ public static string LoadProject_UI_Message_Error_Title_ProjectLoadFailed {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_ProjectLoadFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Session Load Failed.
+ ///
+ public static string LoadProject_UI_Message_Error_Title_SessionLoadFailed {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_SessionLoadFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Session Updated.
+ ///
+ public static string LoadProject_UI_Message_Error_Title_UpdateSessionFile {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Error_Title_UpdateSessionFile", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to None of the files in this session could be found. The session cannot be loaded..
+ ///
+ public static string LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound {
+ get {
+ return ResourceManager.GetString("LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Could not begin restart session. Unable to determine file locker..
///
@@ -3695,6 +3766,141 @@ public static string LogWindow_UI_WriteFilterToTab_NamePrefix_ForFilter {
}
}
+ ///
+ /// Looks up a localized string similar to Close existing tabs.
+ ///
+ public static string MissingFilesDialog_UI_Button_CloseTabs {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Button_CloseTabs", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Ignore layout data.
+ ///
+ public static string MissingFilesDialog_UI_Button_Ignore {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Button_Ignore", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Load && Update Session.
+ ///
+ public static string MissingFilesDialog_UI_Button_LoadUpdateSession {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Button_LoadUpdateSession", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Open new window.
+ ///
+ public static string MissingFilesDialog_UI_Button_NewWindow {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Button_NewWindow", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Load && Update Session ({0}).
+ ///
+ public static string MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Alternative.
+ ///
+ public static string MissingFilesDialog_UI_FileStatus_Alternative {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Alternative", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Missing.
+ ///
+ public static string MissingFilesDialog_UI_FileStatus_Missing {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Missing", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Selected.
+ ///
+ public static string MissingFilesDialog_UI_FileStatus_Selected {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Selected", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Valid.
+ ///
+ public static string MissingFilesDialog_UI_FileStatus_Valid {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_FileStatus_Valid", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Log Files (*.lxp)|*.lxp|All Files (*.*)|*.*.
+ ///
+ public static string MissingFilesDialog_UI_Filter_Logfiles {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Filter_Logfiles", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Locate: {0}.
+ ///
+ public static string MissingFilesDialog_UI_Filter_Title {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Filter_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Please choose how to proceed:.
+ ///
+ public static string MissingFilesDialog_UI_Label_ChooseHowToProceed {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Label_ChooseHowToProceed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Restoring layout requires an empty workbench..
+ ///
+ public static string MissingFilesDialog_UI_Label_Informational {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Label_Informational", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Found: {0} of {1} files ({2} missing).
+ ///
+ public static string MissingFilesDialog_UI_Label_Summary {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Label_Summary", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Loading Session.
+ ///
+ public static string MissingFilesDialog_UI_Title {
+ get {
+ return ResourceManager.GetString("MissingFilesDialog_UI_Title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to File name pattern:.
///
@@ -4390,60 +4596,6 @@ public static string Program_UI_Error_Pipe_CannotConnectToFirstInstance {
}
}
- ///
- /// Looks up a localized string similar to Close existing tabs.
- ///
- public static string ProjectLoadDlg_UI_Button_CloseTabs {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Button_CloseTabs", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Ignore layout data.
- ///
- public static string ProjectLoadDlg_UI_Button_Ignore {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Button_Ignore", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Open new window.
- ///
- public static string ProjectLoadDlg_UI_Button_NewWindow {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Button_NewWindow", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Please choose how to proceed:.
- ///
- public static string ProjectLoadDlg_UI_Label_ChooseHowToProceed {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Label_ChooseHowToProceed", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Restoring layout requires an empty workbench..
- ///
- public static string ProjectLoadDlg_UI_Label_Informational {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Label_Informational", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Loading Session.
- ///
- public static string ProjectLoadDlg_UI_Title {
- get {
- return ResourceManager.GetString("ProjectLoadDlg_UI_Title", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to RegEx.htm.
///
diff --git a/src/LogExpert.Resources/Resources.de.resx b/src/LogExpert.Resources/Resources.de.resx
index 10814e45..4d66d6b2 100644
--- a/src/LogExpert.Resources/Resources.de.resx
+++ b/src/LogExpert.Resources/Resources.de.resx
@@ -1689,22 +1689,22 @@ Ein ausgewähltes Tool erscheint in der Iconbar. Alle anderen verfügbaren Tools
Start: {0}
Ende: {1}
-
+
Sitzung laden
-
+
Das Wiederherstellen des Layouts erfordert eine leere Arbeitsfläche.
-
+
Bitte wählen Sie, wie Sie fortfahren möchten:
-
+
Vorhandene Tabs schließen
-
+
Neues Fenster öffnen
-
+
Layoutdaten ignorieren
@@ -1988,7 +1988,13 @@ Regex-Suche/Ersetzen in der aktuell ausgewählten Zeile
Vertrauen bestätigen
- Vertrauen für Plugin entfernen:`n`n{0}`n`nDas Plugin wird nicht geladen, bis es erneut zur Vertrauensliste hinzugefügt wird.`n`nFortfahren?
+ Vertrauen für Plugin entfernen:
+
+{0}
+
+Das Plugin wird nicht geladen, bis es erneut zur Vertrauensliste hinzugefügt wird.
+
+Fortfahren?
Entfernung bestätigen
@@ -2038,4 +2044,75 @@ Regex-Suche/Ersetzen in der aktuell ausgewählten Zeile
Plugin &Trust Management...
+
+ Fehlen
+
+
+ Ungültiges Regex-Muster: {0}
+
+
+ Suchen: {0}
+
+
+ Protokolldateien (*.lxp)|*.lxp|Alle Dateien (*.*)|*.*
+
+
+ && Aktualisierungssitzung laden
+
+
+ && Aktualisierungssitzung laden ({0})
+
+
+ Gefunden: {0} von {1} Dateien ({2} fehlen)
+
+
+ Ausgewählt
+
+
+ Alternative
+
+
+ Gültig
+
+
+ Sitzungsaktualisierung fehlgeschlagen
+
+
+ Sitzungsdatei konnte nicht aktualisiert werden: {0}
+
+
+ Sitzung aktualisiert
+
+
+ Die Sitzungsdatei wurde mit den neuen Dateipfaden aktualisiert.
+
+
+ Das Laden der Sitzung ist fehlgeschlagen
+
+
+ Keine der Dateien in dieser Sitzung konnte gefunden werden. Die Sitzung kann nicht geladen werden.
+
+
+ Das Laden des Projekts ist fehlgeschlagen
+
+
+ Fehler beim Laden der Projektdatei. Die Datei ist möglicherweise beschädigt oder nicht zugänglich.
+
+
+ Standard (einzelne Zeile)
+
+
+ Keine Spaltenaufteilung. Die gesamte Zeile wird in einer einzigen Spalte angezeigt.
+
+
+ Es muss mindestens eine Datei bereitgestellt werden.
+
+
+ Neustart empfohlen
+
+
+ Plugin-Vertrauenskonfiguration aktualisiert.
+
+LogExpert neu starten, um die Änderungen zu übernehmen?
+
\ No newline at end of file
diff --git a/src/LogExpert.Resources/Resources.resx b/src/LogExpert.Resources/Resources.resx
index ff6ae79b..a9133a4f 100644
--- a/src/LogExpert.Resources/Resources.resx
+++ b/src/LogExpert.Resources/Resources.resx
@@ -255,8 +255,7 @@
[Default]
- Error during save of entry.
- {0}
+ Error during save of entry. {0}
No processes are locking the path specified
@@ -1692,22 +1691,22 @@ Checked tools will appear in the icon bar. All other tools are available in the
Start: {0}
End: {1}
-
+
Loading Session
-
+
Restoring layout requires an empty workbench.
-
+
Please choose how to proceed:
-
+
Close existing tabs
-
+
Open new window
-
+
Ignore layout data
@@ -2074,4 +2073,55 @@ Restart LogExpert to apply changes?
Default (single line)
+
+ Error loading project file. The file may be corrupted or inaccessible.
+
+
+ Project Load Failed
+
+
+ None of the files in this session could be found. The session cannot be loaded.
+
+
+ Session Load Failed
+
+
+ Session file has been updated with the new file paths.
+
+
+ Session Updated
+
+
+ Failed to update session file: {0}
+
+
+ Session Update Failed
+
+
+ Valid
+
+
+ Alternative
+
+
+ Selected
+
+
+ Missing
+
+
+ Found: {0} of {1} files ({2} missing)
+
+
+ Load && Update Session ({0})
+
+
+ Load && Update Session
+
+
+ Log Files (*.lxp)|*.lxp|All Files (*.*)|*.*
+
+
+ Locate: {0}
+
\ No newline at end of file
diff --git a/src/LogExpert.Tests/ConfigManagerTest.cs b/src/LogExpert.Tests/ConfigManagerTest.cs
index 6b58183b..0c835a0f 100644
--- a/src/LogExpert.Tests/ConfigManagerTest.cs
+++ b/src/LogExpert.Tests/ConfigManagerTest.cs
@@ -675,8 +675,7 @@ public void Import_WithValidPopulatedSettings_ShouldSucceed ()
// Verify settings were actually imported
Settings currentSettings = _configManager.Settings;
- Assert.That(currentSettings.FilterList.Any(f => f.SearchText == "IMPORT_TEST_FILTER"), Is.True,
- "Imported filter should be present");
+ Assert.That(currentSettings.FilterList.Any(f => f.SearchText == "IMPORT_TEST_FILTER"), Is.True, "Imported filter should be present");
}
[Test]
diff --git a/src/LogExpert.UI/Dialogs/FileStatus.cs b/src/LogExpert.UI/Dialogs/FileStatus.cs
new file mode 100644
index 00000000..cdf8f891
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/FileStatus.cs
@@ -0,0 +1,27 @@
+namespace LogExpert.UI.Dialogs;
+
+///
+/// Represents the status of a file in the missing files dialog.
+///
+public enum FileStatus
+{
+ ///
+ /// File exists and is accessible.
+ ///
+ Valid,
+
+ ///
+ /// File is missing but alternatives are available.
+ ///
+ MissingWithAlternatives,
+
+ ///
+ /// File is missing and no alternatives found.
+ ///
+ Missing,
+
+ ///
+ /// User has manually selected an alternative path.
+ ///
+ AlternativeSelected
+}
diff --git a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
index 0c39bf56..85fecd78 100644
--- a/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
+++ b/src/LogExpert.UI/Dialogs/LogTabWindow/LogTabWindow.cs
@@ -19,7 +19,6 @@
using LogExpert.Core.Interface;
using LogExpert.Dialogs;
using LogExpert.Entities;
-using LogExpert.PluginRegistry.FileSystem;
using LogExpert.UI.Dialogs;
using LogExpert.UI.Entities;
using LogExpert.UI.Extensions;
@@ -41,7 +40,6 @@ internal partial class LogTabWindow : Form, ILogTabWindow
private const int MAX_COLUMNIZER_HISTORY = 40;
private const int MAX_COLOR_HISTORY = 40;
private const int DIFF_MAX = 100;
- private const int MAX_FILE_HISTORY = 10;
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly Icon _deadIcon;
@@ -546,7 +544,7 @@ public LogWindow.LogWindow AddFileTabDeferred (string givenFileName, bool isTemp
[SupportedOSPlatform("windows")]
public LogWindow.LogWindow AddFileTab (string givenFileName, bool isTempFile, string title, bool forcePersistenceLoading, ILogLineMemoryColumnizer preProcessColumnizer, bool doNotAddToDockPanel = false)
{
- var logFileName = FindFilenameForSettings(givenFileName);
+ var logFileName = PersisterHelpers.FindFilenameForSettings(givenFileName, PluginRegistry.PluginRegistry.Instance);
var win = FindWindowForFile(logFileName);
if (win != null)
{
@@ -585,20 +583,21 @@ public LogWindow.LogWindow AddFileTab (string givenFileName, bool isTempFile, st
var data = logWindow.Tag as LogWindowData;
data.Color = _defaultTabColor;
- SetTabColor(logWindow, _defaultTabColor);
+ //TODO SetTabColor and the Coloring must be reimplemented with a different UI Framework
+ //SetTabColor(logWindow, _defaultTabColor);
//data.tabPage.BorderColor = this.defaultTabBorderColor;
- if (!isTempFile)
- {
- foreach (var colorEntry in ConfigManager.Settings.FileColors)
- {
- if (colorEntry.FileName.ToUpperInvariant().Equals(logFileName.ToUpperInvariant(), StringComparison.Ordinal))
- {
- data.Color = colorEntry.Color;
- SetTabColor(logWindow, colorEntry.Color);
- break;
- }
- }
- }
+ //if (!isTempFile)
+ //{
+ // foreach (var colorEntry in ConfigManager.Settings.FileColors)
+ // {
+ // if (colorEntry.FileName.ToUpperInvariant().Equals(logFileName.ToUpperInvariant(), StringComparison.Ordinal))
+ // {
+ // data.Color = colorEntry.Color;
+ // //SetTabColor(logWindow, colorEntry.Color);
+ // break;
+ // }
+ // }
+ //}
if (!isTempFile)
{
@@ -910,8 +909,7 @@ private void DestroyBookmarkWindow ()
private void SaveLastOpenFilesList ()
{
- ConfigManager.Settings.LastOpenFilesList.Clear();
- foreach (DockContent content in dockPanel.Contents)
+ foreach (DockContent content in dockPanel.Contents.Cast())
{
if (content is LogWindow.LogWindow logWin)
{
@@ -1045,24 +1043,7 @@ private void DisconnectEventHandlers (LogWindow.LogWindow logWindow)
[SupportedOSPlatform("windows")]
private void AddToFileHistory (string fileName)
{
- bool FindName (string s) => s.ToUpperInvariant().Equals(fileName.ToUpperInvariant(), StringComparison.Ordinal);
-
- var index = ConfigManager.Settings.FileHistoryList.FindIndex(FindName);
-
- if (index != -1)
- {
- ConfigManager.Settings.FileHistoryList.RemoveAt(index);
- }
-
- ConfigManager.Settings.FileHistoryList.Insert(0, fileName);
-
- while (ConfigManager.Settings.FileHistoryList.Count > MAX_FILE_HISTORY)
- {
- ConfigManager.Settings.FileHistoryList.RemoveAt(ConfigManager.Settings.FileHistoryList.Count - 1);
- }
-
- ConfigManager.Save(SettingsFlags.FileHistory);
-
+ ConfigManager.AddToFileHistory(fileName);
FillHistoryMenu();
}
@@ -1083,46 +1064,6 @@ private LogWindow.LogWindow FindWindowForFile (string fileName)
return null;
}
- ///
- /// Checks if the file name is a settings file. If so, the contained logfile name
- /// is returned. If not, the given file name is returned unchanged.
- ///
- ///
- ///
- private static string FindFilenameForSettings (string fileName)
- {
- if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase))
- {
- var persistenceData = Persister.Load(fileName);
- if (persistenceData == null)
- {
- return fileName;
- }
-
- if (!string.IsNullOrEmpty(persistenceData.FileName))
- {
- var fs = PluginRegistry.PluginRegistry.Instance.FindFileSystemForUri(persistenceData.FileName);
- if (fs != null && !fs.GetType().Equals(typeof(LocalFileSystem)))
- {
- return persistenceData.FileName;
- }
-
- // On relative paths the URI check (and therefore the file system plugin check) will fail.
- // So fs == null and fs == LocalFileSystem are handled here like normal files.
- if (Path.IsPathRooted(persistenceData.FileName))
- {
- return persistenceData.FileName;
- }
-
- // handle relative paths in .lxp files
- var dir = Path.GetDirectoryName(fileName);
- return Path.Join(dir, persistenceData.FileName);
- }
- }
-
- return fileName;
- }
-
[SupportedOSPlatform("windows")]
private void FillHistoryMenu ()
{
@@ -1372,7 +1313,7 @@ private void ChangeCurrentLogWindow (LogWindow.LogWindow newLogWindow)
oldLogWindow.BookmarkAdded -= OnBookmarkAdded;
oldLogWindow.BookmarkRemoved -= OnBookmarkRemoved;
oldLogWindow.BookmarkTextChanged -= OnBookmarkTextChanged;
- DisconnectToolWindows(oldLogWindow);
+ DisconnectToolWindows();
}
if (newLogWindow != null)
@@ -1434,13 +1375,12 @@ private void ConnectBookmarkWindow (LogWindow.LogWindow logWindow)
_bookmarkWindow.SetCurrentFile(ctx);
}
- private void DisconnectToolWindows (LogWindow.LogWindow logWindow)
+ private void DisconnectToolWindows ()
{
- DisconnectBookmarkWindow(logWindow);
+ DisconnectBookmarkWindow();
}
- //TODO Find out if logwindow is necessary here
- private void DisconnectBookmarkWindow (LogWindow.LogWindow logWindow)
+ private void DisconnectBookmarkWindow ()
{
_bookmarkWindow.SetBookmarkData(null);
_bookmarkWindow.SetCurrentFile(null);
@@ -1460,6 +1400,7 @@ private void GuiStateUpdateWorker (GuiStateEventArgs e)
multiFileToolStripMenuItem.Checked = e.IsMultiFileActive;
multiFileEnabledStripMenuItem.Checked = e.IsMultiFileActive;
cellSelectModeToolStripMenuItem.Checked = e.CellSelectMode;
+
RefreshEncodingMenuBar(e.CurrentEncoding);
if (e.TimeshiftPossible && ConfigManager.Settings.Preferences.TimestampControl)
@@ -1679,6 +1620,7 @@ private static int GetLevelFromDiff (int diff)
}
[SupportedOSPlatform("windows")]
+ //TODO Task based
private void LedThreadProc ()
{
Thread.CurrentThread.Name = "LED Thread";
@@ -2002,44 +1944,96 @@ private void CloseAllTabs ()
}
//TODO Reimplementation needs a new UI Framework since, DockpanelSuite has no easy way to change TabColor
- private static void SetTabColor (LogWindow.LogWindow logWindow, Color color)
- {
- //tabPage.BackLowColor = color;
- //tabPage.BackLowColorDisabled = Color.FromArgb(255,
- // Math.Max(0, color.R - 50),
- // Math.Max(0, color.G - 50),
- // Math.Max(0, color.B - 50)
- // );
- }
+ //private static void SetTabColor (LogWindow.LogWindow logWindow, Color color)
+ //{
+ // //tabPage.BackLowColor = color;
+ // //tabPage.BackLowColorDisabled = Color.FromArgb(255,
+ // // Math.Max(0, color.R - 50),
+ // // Math.Max(0, color.G - 50),
+ // // Math.Max(0, color.B - 50)
+ // // );
+ //}
[SupportedOSPlatform("windows")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0010:Add missing cases", Justification = "no need for the other switch cases")]
private void LoadProject (string projectFileName, bool restoreLayout)
{
- var projectData = ProjectPersister.LoadProjectData(projectFileName);
- var hasLayoutData = projectData.TabLayoutXml != null;
-
- if (hasLayoutData && restoreLayout && _logWindowList.Count > 0)
+ try
{
- ProjectLoadDlg dlg = new();
- if (DialogResult.Cancel != dlg.ShowDialog())
+ // Load project with validation
+ var loadResult = ProjectPersister.LoadProjectData(projectFileName, PluginRegistry.PluginRegistry.Instance);
+
+ if (loadResult?.ProjectData == null)
+ {
+ ShowOkMessage(
+ Resources.LoadProject_UI_Message_Error_FileMaybeCorruptedOrInaccessible,
+ Resources.LoadProject_UI_Message_Error_Title_ProjectLoadFailed,
+ MessageBoxIcon.Error);
+
+ return;
+ }
+
+ var projectData = loadResult.ProjectData;
+ var hasLayoutData = projectData.TabLayoutXml != null;
+
+ if (projectData.FileNames.Count == 0)
{
- switch (dlg.ProjectLoadResult)
+ ShowOkMessage(
+ Resources.LoadProject_UI_Message_Error_Title_SessionLoadFailed,
+ Resources.LoadProject_UI_Message_Message_FilesForSessionCouldNotBeFound,
+ MessageBoxIcon.Error);
+ return;
+ }
+
+ // Handle missing files or layout options
+ if (loadResult.RequiresUserIntervention)
+ {
+ // Show enhanced dialog with browsing capability and layout options
+ var (dialogResult, updateSessionFile, selectedAlternatives) = MissingFilesDialog.ShowDialog(loadResult.ValidationResult, hasLayoutData);
+
+ if (dialogResult == MissingFilesDialogResult.Cancel)
{
- case ProjectLoadDlgResult.IgnoreLayout:
- hasLayoutData = false;
- break;
- case ProjectLoadDlgResult.CloseTabs:
+ return;
+ }
+
+ if (updateSessionFile)
+ {
+ // Replace original paths with selected alternatives in project data
+ for (int i = 0; i < projectData.FileNames.Count; i++)
+ {
+ var originalPath = projectData.FileNames[i];
+ if (selectedAlternatives.TryGetValue(originalPath, out string value))
+ {
+ projectData.FileNames[i] = value;
+ }
+ }
+
+ ProjectPersister.SaveProjectData(projectFileName, projectData);
+
+ ShowOkMessage(
+ Resources.LoadProject_UI_Message_Error_Message_UpdateSessionFile,
+ Resources.LoadProject_UI_Message_Error_Title_UpdateSessionFile,
+ MessageBoxIcon.Information);
+ }
+
+ // Handle layout-related results
+ switch (dialogResult)
+ {
+ case MissingFilesDialogResult.CloseTabsAndRestoreLayout:
CloseAllTabs();
break;
- case ProjectLoadDlgResult.NewWindow:
- LogExpertProxy.NewWindow([projectFileName]);
- return;
+ case MissingFilesDialogResult.OpenInNewWindow:
+ {
+ var logFileNames = PersisterHelpers.FindFilenameForSettings(projectData.FileNames.AsReadOnly(), PluginRegistry.PluginRegistry.Instance);
+ LogExpertProxy.NewWindow([.. logFileNames]);
+ return;
+ }
+ case MissingFilesDialogResult.IgnoreLayout:
+ hasLayoutData = false;
+ break;
}
}
- }
- if (projectData != null)
- {
foreach (var fileName in projectData.FileNames)
{
_ = hasLayoutData
@@ -2047,16 +2041,38 @@ private void LoadProject (string projectFileName, bool restoreLayout)
: AddFileTab(fileName, false, null, true, null);
}
- if (hasLayoutData && restoreLayout)
+ // Restore layout only if we loaded at least one file
+ if (hasLayoutData && restoreLayout && _logWindowList.Count > 0)
{
+ _logger.Info("Restoring layout");
// Re-creating tool (non-document) windows is needed because the DockPanel control would throw strange errors
DestroyToolWindows();
InitToolWindows();
RestoreLayout(projectData.TabLayoutXml);
}
+ else if (_logWindowList.Count == 0)
+ {
+ _logger.Warn("No files loaded, skipping layout restoration");
+ }
+ }
+ catch (Exception ex)
+ {
+ ShowOkMessage(
+ $"Error loading project: {ex.Message}",
+ Resources.LogExpert_Common_UI_Title_Error,
+ MessageBoxIcon.Error);
}
}
+ private static void ShowOkMessage (string title, string message, MessageBoxIcon icon)
+ {
+ _ = MessageBox.Show(
+ message,
+ title,
+ MessageBoxButtons.OK,
+ icon);
+ }
+
[SupportedOSPlatform("windows")]
private void ApplySelectedHighlightGroup ()
{
@@ -2229,6 +2245,8 @@ private void OnLogTabWindowLoad (object sender, EventArgs e)
AddFileTab(name, false, null, false, null);
}
}
+
+ ConfigManager.ClearLastOpenFilesList();
}
if (_startupFileNames != null)
@@ -2623,8 +2641,7 @@ private void OnFileSizeChanged (object sender, LogEventArgs e)
//if (this.dockPanel.ActiveContent != null &&
// this.dockPanel.ActiveContent != sender || data.tailState != 0)
- if ((CurrentLogWindow != null &&
- CurrentLogWindow != sender) || data.TailState != 0)
+ if (CurrentLogWindow != null && CurrentLogWindow != sender || data.TailState != 0)
{
data.Dirty = true;
}
@@ -2928,45 +2945,46 @@ private void OnCloseAllTabsToolStripMenuItemClick (object sender, EventArgs e)
[SupportedOSPlatform("windows")]
private void OnTabColorToolStripMenuItemClick (object sender, EventArgs e)
{
- var logWindow = dockPanel.ActiveContent as LogWindow.LogWindow;
+ //Todo TabColoring must be reimplemented with a different UI Framework
+ //var logWindow = dockPanel.ActiveContent as LogWindow.LogWindow;
- if (logWindow.Tag is not LogWindowData data)
- {
- return;
- }
+ //if (logWindow.Tag is not LogWindowData data)
+ //{
+ // return;
+ //}
- ColorDialog dlg = new()
- {
- Color = data.Color
- };
+ //ColorDialog dlg = new()
+ //{
+ // Color = data.Color
+ //};
- if (dlg.ShowDialog() == DialogResult.OK)
- {
- data.Color = dlg.Color;
- SetTabColor(logWindow, data.Color);
- }
+ //if (dlg.ShowDialog() == DialogResult.OK)
+ //{
+ // data.Color = dlg.Color;
+ // //SetTabColor(logWindow, data.Color);
+ //}
- List delList = [];
+ //List delList = [];
- foreach (var entry in ConfigManager.Settings.FileColors)
- {
- if (entry.FileName.Equals(logWindow.FileName, StringComparison.Ordinal))
- {
- delList.Add(entry);
- }
- }
+ //foreach (var entry in ConfigManager.Settings.FileColors)
+ //{
+ // if (entry.FileName.Equals(logWindow.FileName, StringComparison.Ordinal))
+ // {
+ // delList.Add(entry);
+ // }
+ //}
- foreach (var entry in delList)
- {
- _ = ConfigManager.Settings.FileColors.Remove(entry);
- }
+ //foreach (var entry in delList)
+ //{
+ // _ = ConfigManager.Settings.FileColors.Remove(entry);
+ //}
- ConfigManager.Settings.FileColors.Add(new ColorEntry(logWindow.FileName, dlg.Color));
+ //ConfigManager.Settings.FileColors.Add(new ColorEntry(logWindow.FileName, dlg.Color));
- while (ConfigManager.Settings.FileColors.Count > MAX_COLOR_HISTORY)
- {
- ConfigManager.Settings.FileColors.RemoveAt(0);
- }
+ //while (ConfigManager.Settings.FileColors.Count > MAX_COLOR_HISTORY)
+ //{
+ // ConfigManager.Settings.FileColors.RemoveAt(0);
+ //}
}
[SupportedOSPlatform("windows")]
diff --git a/src/LogExpert.UI/Dialogs/MissingFileItem.cs b/src/LogExpert.UI/Dialogs/MissingFileItem.cs
new file mode 100644
index 00000000..73c35497
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFileItem.cs
@@ -0,0 +1,61 @@
+namespace LogExpert.UI.Dialogs;
+
+///
+/// Represents a file item in the Missing Files Dialog ListView.
+///
+public class MissingFileItem
+{
+ ///
+ /// Original file path from the session/project file.
+ ///
+ public string OriginalPath { get; set; }
+
+ ///
+ /// Current status of the file.
+ ///
+ public FileStatus Status { get; set; }
+
+ ///
+ /// List of alternative paths that might be the same file.
+ ///
+ public List Alternatives { get; set; } = [];
+
+ ///
+ /// Currently selected path (original or alternative).
+ ///
+ public string SelectedPath { get; set; }
+
+ ///
+ /// Indicates whether the file is accessible.
+ ///
+ public bool IsAccessible => Status is FileStatus.Valid or FileStatus.AlternativeSelected;
+
+ ///
+ /// Gets the display name for the ListView (just the filename).
+ ///
+ public string DisplayName => Path.GetFileName(OriginalPath) ?? OriginalPath;
+
+ ///
+ /// Gets the status text for display.
+ ///
+ public string StatusText => Status switch
+ {
+ FileStatus.Valid => "Found",
+ FileStatus.MissingWithAlternatives => $"Missing ({Alternatives.Count} alternatives)",
+ FileStatus.Missing => "Missing",
+ 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/MissingFilesDialog.Designer.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialog.Designer.cs
new file mode 100644
index 00000000..1ae13b31
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.Designer.cs
@@ -0,0 +1,311 @@
+using System.Runtime.Versioning;
+
+namespace LogExpert.UI.Dialogs;
+
+[SupportedOSPlatform("windows")]
+partial class MissingFilesDialog
+{
+ ///
+ /// Required designer variable.
+ ///
+ 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?.Dispose();
+ imageListStatus?.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.
+ ///
+ private void InitializeComponent()
+ {
+ components = new System.ComponentModel.Container();
+
+ listViewFiles = new ListView();
+ columnFileName = new ColumnHeader();
+ columnStatus = new ColumnHeader();
+ columnPath = new ColumnHeader();
+ buttonLoadAndUpdate = new Button();
+ buttonLoad = new Button();
+ buttonBrowse = new Button();
+ buttonCancel = new Button();
+ labelInfo = new Label();
+ labelSummary = new Label();
+ imageListStatus = new ImageList(components);
+ panelButtons = new Panel();
+ panelTop = new Panel();
+ panelLayoutOptions = new Panel();
+ labelLayoutInfo = new Label();
+ radioButtonCloseTabs = new RadioButton();
+ radioButtonNewWindow = new RadioButton();
+ radioButtonIgnoreLayout = new RadioButton();
+ buttonLayoutPanel = new FlowLayoutPanel();
+
+ SuspendLayout();
+ panelLayoutOptions.SuspendLayout();
+ panelTop.SuspendLayout();
+ panelButtons.SuspendLayout();
+
+ //
+ // imageListStatus
+ //
+ imageListStatus.ColorDepth = ColorDepth.Depth32Bit;
+ imageListStatus.ImageSize = new Size(16, 16);
+ CreateStatusIcons();
+
+ //
+ // panelTop
+ //
+ panelTop.Controls.Add(labelSummary);
+ panelTop.Controls.Add(labelInfo);
+ panelTop.Dock = DockStyle.Top;
+ panelTop.Height = 80;
+ panelTop.Padding = new Padding(10);
+ panelTop.TabIndex = 0;
+
+ //
+ // labelInfo
+ //
+ labelInfo.AutoSize = false;
+ labelInfo.Dock = DockStyle.Top;
+ labelInfo.Height = 40;
+ labelInfo.Text = "Some files from the session could not be found. You can browse for missing files or load only the files that were found.";
+ labelInfo.TextAlign = ContentAlignment.MiddleLeft;
+ labelInfo.TabIndex = 0;
+
+ //
+ // labelSummary
+ //
+ labelSummary.AutoSize = false;
+ labelSummary.Dock = DockStyle.Top;
+ labelSummary.Font = new Font(Font, FontStyle.Bold);
+ labelSummary.Height = 30;
+ labelSummary.TextAlign = ContentAlignment.MiddleLeft;
+ labelSummary.TabIndex = 1;
+
+ //
+ // labelLayoutInfo
+ //
+ labelLayoutInfo.AutoSize = false;
+ labelLayoutInfo.Location = new Point(10, 5);
+ labelLayoutInfo.Size = new Size(400, 25);
+ labelLayoutInfo.Text = "This session contains layout data. How would you like to proceed?";
+ labelLayoutInfo.TextAlign = ContentAlignment.MiddleLeft;
+ labelLayoutInfo.TabIndex = 0;
+
+ //
+ // radioButtonCloseTabs
+ //
+ radioButtonCloseTabs.AutoSize = true;
+ radioButtonCloseTabs.Checked = true;
+ radioButtonCloseTabs.Location = new Point(10, 35);
+ radioButtonCloseTabs.Name = "radioButtonCloseTabs";
+ radioButtonCloseTabs.Size = new Size(200, 24);
+ radioButtonCloseTabs.TabIndex = 1;
+ radioButtonCloseTabs.TabStop = true;
+ radioButtonCloseTabs.Text = "Close existing tabs and restore layout";
+ radioButtonCloseTabs.UseVisualStyleBackColor = true;
+
+ //
+ // radioButtonNewWindow
+ //
+ radioButtonNewWindow.AutoSize = true;
+ radioButtonNewWindow.Location = new Point(10, 60);
+ radioButtonNewWindow.Name = "radioButtonNewWindow";
+ radioButtonNewWindow.Size = new Size(200, 24);
+ radioButtonNewWindow.TabIndex = 2;
+ radioButtonNewWindow.Text = "Open in a new window";
+ radioButtonNewWindow.UseVisualStyleBackColor = true;
+
+ //
+ // radioButtonIgnoreLayout
+ //
+ radioButtonIgnoreLayout.AutoSize = true;
+ radioButtonIgnoreLayout.Location = new Point(10, 85);
+ radioButtonIgnoreLayout.Name = "radioButtonIgnoreLayout";
+ radioButtonIgnoreLayout.Size = new Size(200, 24);
+ radioButtonIgnoreLayout.TabIndex = 3;
+ radioButtonIgnoreLayout.Text = "Ignore layout data";
+ radioButtonIgnoreLayout.UseVisualStyleBackColor = true;
+
+ //
+ // panelLayoutOptions
+ //
+ panelLayoutOptions.Controls.Add(radioButtonIgnoreLayout);
+ panelLayoutOptions.Controls.Add(radioButtonNewWindow);
+ panelLayoutOptions.Controls.Add(radioButtonCloseTabs);
+ panelLayoutOptions.Controls.Add(labelLayoutInfo);
+ panelLayoutOptions.Dock = DockStyle.Bottom;
+ panelLayoutOptions.Height = 115;
+ panelLayoutOptions.TabIndex = 3;
+ panelLayoutOptions.Visible = false;
+
+ //
+ // listViewFiles
+ //
+ listViewFiles.Columns.AddRange(columnFileName, columnStatus, columnPath);
+ listViewFiles.Dock = DockStyle.Fill;
+ listViewFiles.FullRowSelect = true;
+ listViewFiles.GridLines = true;
+ listViewFiles.MultiSelect = false;
+ listViewFiles.SmallImageList = imageListStatus;
+ listViewFiles.TabIndex = 1;
+ listViewFiles.View = View.Details;
+ listViewFiles.SelectedIndexChanged += OnListViewSelectedIndexChanged;
+ listViewFiles.DoubleClick += OnListViewDoubleClick;
+
+ //
+ // columnFileName
+ //
+ columnFileName.Text = "File Name";
+ columnFileName.Width = 200;
+
+ //
+ // columnStatus
+ //
+ columnStatus.Text = "Status";
+ columnStatus.Width = 150;
+
+ //
+ // columnPath
+ //
+ columnPath.Text = "Path";
+ columnPath.Width = 400;
+
+ //
+ // buttonLoad
+ //
+ buttonLoad.AutoSize = true;
+ buttonLoad.Height = 30;
+ buttonLoad.Margin = new Padding(3);
+ buttonLoad.MinimumSize = new Size(100, 30);
+ buttonLoad.TabIndex = 0;
+ buttonLoad.Text = "Load Files";
+ buttonLoad.UseVisualStyleBackColor = true;
+ buttonLoad.Click += OnButtonLoadClick;
+
+ //
+ // buttonBrowse
+ //
+ buttonBrowse.AutoSize = true;
+ buttonBrowse.Enabled = false;
+ buttonBrowse.Height = 30;
+ buttonBrowse.Margin = new Padding(3);
+ buttonBrowse.MinimumSize = new Size(100, 30);
+ buttonBrowse.TabIndex = 1;
+ buttonBrowse.Text = "Browse...";
+ buttonBrowse.UseVisualStyleBackColor = true;
+ buttonBrowse.Click += OnButtonBrowseClick;
+
+ //
+ // buttonLoadAndUpdate
+ //
+ buttonLoadAndUpdate.AutoSize = true;
+ buttonLoadAndUpdate.Enabled = false;
+ buttonLoadAndUpdate.Height = 30;
+ buttonLoadAndUpdate.Margin = new Padding(3);
+ buttonLoadAndUpdate.MinimumSize = new Size(150, 30);
+ buttonLoadAndUpdate.TabIndex = 2;
+ buttonLoadAndUpdate.Text = "Load && Update Session";
+ buttonLoadAndUpdate.UseVisualStyleBackColor = true;
+ buttonLoadAndUpdate.Click += OnButtonLoadAndUpdateClick;
+
+ //
+ // buttonCancel
+ //
+ buttonCancel.AutoSize = true;
+ buttonCancel.DialogResult = DialogResult.Cancel;
+ buttonCancel.Height = 30;
+ buttonCancel.Margin = new Padding(3);
+ buttonCancel.MinimumSize = new Size(100, 30);
+ buttonCancel.TabIndex = 3;
+ buttonCancel.Text = "Cancel";
+ buttonCancel.UseVisualStyleBackColor = true;
+ buttonCancel.Click += OnButtonCancelClick;
+
+ //
+ // buttonLayoutPanel
+ //
+ buttonLayoutPanel.AutoSize = true;
+ buttonLayoutPanel.AutoSizeMode = AutoSizeMode.GrowAndShrink;
+ buttonLayoutPanel.Controls.Add(buttonLoad);
+ buttonLayoutPanel.Controls.Add(buttonBrowse);
+ buttonLayoutPanel.Controls.Add(buttonLoadAndUpdate);
+ buttonLayoutPanel.Controls.Add(buttonCancel);
+ buttonLayoutPanel.Dock = DockStyle.Right;
+ buttonLayoutPanel.FlowDirection = FlowDirection.LeftToRight;
+ buttonLayoutPanel.Location = new Point(0, 10);
+ buttonLayoutPanel.Padding = new Padding(10, 10, 10, 10);
+ buttonLayoutPanel.TabIndex = 0;
+ buttonLayoutPanel.WrapContents = false;
+
+ //
+ // panelButtons
+ //
+ panelButtons.Controls.Add(buttonLayoutPanel);
+ panelButtons.Dock = DockStyle.Bottom;
+ panelButtons.Height = 60;
+ panelButtons.TabIndex = 2;
+
+ //
+ // MissingFilesDialog
+ //
+ AcceptButton = buttonLoad;
+ AutoScaleDimensions = new SizeF(96F, 96F);
+ AutoScaleMode = AutoScaleMode.Dpi;
+ CancelButton = buttonCancel;
+ ClientSize = new Size(840, 500);
+ Controls.Add(listViewFiles);
+ Controls.Add(panelLayoutOptions);
+ Controls.Add(panelButtons);
+ Controls.Add(panelTop);
+ FormBorderStyle = FormBorderStyle.Sizable;
+ MinimumSize = new Size(600, 400);
+ ShowIcon = false;
+ ShowInTaskbar = false;
+ StartPosition = FormStartPosition.CenterParent;
+ Text = "Missing Files";
+
+ panelLayoutOptions.ResumeLayout(false);
+ panelLayoutOptions.PerformLayout();
+ panelTop.ResumeLayout(false);
+ panelButtons.ResumeLayout(false);
+ panelButtons.PerformLayout();
+ ResumeLayout(false);
+ }
+
+ #endregion
+
+ private ListView listViewFiles;
+ private ColumnHeader columnFileName;
+ private ColumnHeader columnStatus;
+ private ColumnHeader columnPath;
+ private Button buttonLoad;
+ private Button buttonLoadAndUpdate;
+ private Button buttonBrowse;
+ private Button buttonCancel;
+ private Label labelInfo;
+ private Label labelSummary;
+ private ImageList imageListStatus;
+ private Panel panelButtons;
+ private Panel panelTop;
+ private Panel panelLayoutOptions;
+ private Label labelLayoutInfo;
+ private RadioButton radioButtonCloseTabs;
+ private RadioButton radioButtonNewWindow;
+ private RadioButton radioButtonIgnoreLayout;
+ private FlowLayoutPanel buttonLayoutPanel;
+}
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs
new file mode 100644
index 00000000..04d8b34d
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.cs
@@ -0,0 +1,421 @@
+using System.Globalization;
+using System.Runtime.Versioning;
+
+using LogExpert.Core.Classes.Persister;
+
+namespace LogExpert.UI.Dialogs;
+
+///
+/// Enhanced dialog for handling missing files with browsing and alternative selection.
+/// Also handles layout restoration options when loading a project with existing tabs.
+/// Phase 2 implementation of the Project File Validator.
+///
+[SupportedOSPlatform("windows")]
+public partial class MissingFilesDialog : Form
+{
+ #region Fields
+
+ private readonly ProjectValidationResult _validationResult;
+ private readonly Dictionary _fileItems;
+ private readonly bool _hasLayoutData;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets the dialog result indicating the user's choice.
+ ///
+ public MissingFilesDialogResult Result { get; private set; }
+
+ ///
+ /// Gets whether the user wants to update the session file.
+ ///
+ public bool UpdateSessionFile { get; private set; }
+
+ ///
+ /// Gets the dictionary of selected alternative paths for missing files.
+ /// Key: original path, Value: selected alternative path
+ ///
+ public Dictionary SelectedAlternatives { get; private set; }
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// Constructor for MissingFilesDialog.
+ ///
+ /// Validation result containing file information
+ /// Whether to show layout restoration options
+ /// Whether the project has layout data to restore
+ public MissingFilesDialog (ProjectValidationResult validationResult, bool hasLayoutData = false)
+ {
+ ArgumentNullException.ThrowIfNull(validationResult);
+
+ _validationResult = validationResult;
+ _fileItems = [];
+ SelectedAlternatives = [];
+ Result = MissingFilesDialogResult.Cancel;
+ UpdateSessionFile = false;
+ _hasLayoutData = hasLayoutData;
+
+ InitializeComponent();
+ InitializeFileItems();
+ PopulateListView();
+ UpdateSummary();
+ ConfigureLayoutOptions();
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Shows the dialog with layout options and returns alternatives if selected.
+ ///
+ /// Validation result
+ /// Whether the project has layout data
+ /// Tuple containing the dialog result, whether to update session file, and selected alternatives
+ public static (MissingFilesDialogResult Result, bool UpdateSessionFile, Dictionary SelectedAlternatives) ShowDialog (ProjectValidationResult validationResult, bool hasLayoutData)
+ {
+ using var dialog = new MissingFilesDialog(validationResult, hasLayoutData);
+ _ = dialog.ShowDialog();
+ return (dialog.Result, dialog.UpdateSessionFile, dialog.SelectedAlternatives);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// Configures visibility and state of layout options panel.
+ ///
+ private void ConfigureLayoutOptions ()
+ {
+ Text = Resources.MissingFilesDialog_UI_Title;
+
+ panelLayoutOptions.Visible = true;
+ labelLayoutInfo.Text = Resources.MissingFilesDialog_UI_Label_Informational;
+ radioButtonCloseTabs.Text = Resources.MissingFilesDialog_UI_Button_CloseTabs;
+ radioButtonNewWindow.Text = Resources.MissingFilesDialog_UI_Button_NewWindow;
+ radioButtonIgnoreLayout.Text = Resources.MissingFilesDialog_UI_Button_Ignore;
+ radioButtonCloseTabs.Checked = true;
+ panelLayoutOptions.BringToFront();
+ Height += panelLayoutOptions.Height;
+ }
+
+ ///
+ /// Creates status icons for the ImageList.
+ ///
+ private void CreateStatusIcons ()
+ {
+ // Create simple colored circles as status indicators
+
+ // Valid - Green circle
+ var validIcon = new Bitmap(16, 16);
+ using (var g = Graphics.FromImage(validIcon))
+ {
+ g.Clear(Color.Transparent);
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ g.FillEllipse(Brushes.Green, 2, 2, 12, 12);
+ }
+
+ imageListStatus.Images.Add("Valid", validIcon);
+
+ // Missing - Red circle
+ var missingIcon = new Bitmap(16, 16);
+ using (var g = Graphics.FromImage(missingIcon))
+ {
+ g.Clear(Color.Transparent);
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ g.FillEllipse(Brushes.Red, 2, 2, 12, 12);
+ }
+
+ imageListStatus.Images.Add("Missing", missingIcon);
+
+ // Alternative available - Orange circle
+ var alternativeIcon = new Bitmap(16, 16);
+ using (var g = Graphics.FromImage(alternativeIcon))
+ {
+ g.Clear(Color.Transparent);
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ g.FillEllipse(Brushes.Orange, 2, 2, 12, 12);
+ }
+
+ imageListStatus.Images.Add("Alternative", alternativeIcon);
+
+ // Alternative selected - Blue circle
+ var selectedIcon = new Bitmap(16, 16);
+ using (var g = Graphics.FromImage(selectedIcon))
+ {
+ g.Clear(Color.Transparent);
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
+ g.FillEllipse(Brushes.Blue, 2, 2, 12, 12);
+ }
+
+ imageListStatus.Images.Add("Selected", selectedIcon);
+ }
+
+ ///
+ /// Initializes the file items dictionary from validation result.
+ ///
+ private void InitializeFileItems ()
+ {
+ // Add valid files
+ foreach (var validPath in _validationResult.ValidFiles)
+ {
+ var item = new MissingFileItem(validPath, FileStatus.Valid);
+ _fileItems[validPath] = item;
+ }
+
+ // Add missing files
+ foreach (var missingPath in _validationResult.MissingFiles)
+ {
+ var alternatives = _validationResult.PossibleAlternatives.TryGetValue(missingPath, out List? value)
+ ? value
+ : [];
+
+ var status = alternatives.Count > 0
+ ? FileStatus.MissingWithAlternatives
+ : FileStatus.Missing;
+
+ var item = new MissingFileItem(missingPath, status)
+ {
+ Alternatives = alternatives
+ };
+
+ _fileItems[missingPath] = item;
+ }
+ }
+
+ ///
+ /// Populates the ListView with file items.
+ ///
+ private void PopulateListView ()
+ {
+ listViewFiles.BeginUpdate();
+ listViewFiles.Items.Clear();
+
+ foreach (var fileItem in _fileItems.Values)
+ {
+ var listItem = new ListViewItem(fileItem.DisplayName)
+ {
+ Tag = fileItem,
+ ImageKey = fileItem.Status switch
+ {
+ FileStatus.Valid => Resources.MissingFilesDialog_UI_FileStatus_Valid,
+ FileStatus.MissingWithAlternatives => Resources.MissingFilesDialog_UI_FileStatus_Alternative,
+ FileStatus.AlternativeSelected => Resources.MissingFilesDialog_UI_FileStatus_Selected,
+ FileStatus.Missing => Resources.MissingFilesDialog_UI_FileStatus_Missing,
+ _ => Resources.MissingFilesDialog_UI_FileStatus_Missing
+ }
+ };
+
+ _ = listItem.SubItems.Add(fileItem.StatusText);
+ _ = listItem.SubItems.Add(fileItem.SelectedPath);
+
+ // Color code the row based on status
+ if (fileItem.Status == FileStatus.Missing)
+ {
+ listItem.ForeColor = Color.Red;
+ }
+ else if (fileItem.Status == FileStatus.MissingWithAlternatives)
+ {
+ listItem.ForeColor = Color.DarkOrange;
+ }
+ else if (fileItem.Status == FileStatus.AlternativeSelected)
+ {
+ listItem.ForeColor = Color.Blue;
+ }
+
+ _ = listViewFiles.Items.Add(listItem);
+ }
+
+ listViewFiles.EndUpdate();
+ }
+
+ ///
+ /// Updates the summary label and control states.
+ ///
+ private void UpdateSummary ()
+ {
+ var validCount = _fileItems.Values.Count(f => f.IsAccessible);
+ var totalCount = _fileItems.Count;
+ var missingCount = totalCount - validCount;
+
+ labelSummary.Text = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Label_Summary, validCount, totalCount, missingCount);
+
+ // Enable "Load and Update Session" only if user has selected alternatives
+ var hasSelectedAlternatives = _fileItems.Values.Any(f => f.Status == FileStatus.AlternativeSelected);
+ buttonLoadAndUpdate.Enabled = hasSelectedAlternatives;
+
+ // Update button text based on selection
+ if (hasSelectedAlternatives)
+ {
+ var alternativeCount = _fileItems.Values.Count(f => f.Status == FileStatus.AlternativeSelected);
+ buttonLoadAndUpdate.Text = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Button_UpdateSessionAlternativeCount, alternativeCount);
+ }
+ else
+ {
+ buttonLoadAndUpdate.Text = Resources.MissingFilesDialog_UI_Button_LoadUpdateSession;
+ }
+ }
+
+ ///
+ /// Opens a file browser dialog for the specified missing file.
+ ///
+ /// The file item to browse for
+ private void BrowseForFile (MissingFileItem fileItem)
+ {
+ using var openFileDialog = new OpenFileDialog
+ {
+ Title = string.Format(CultureInfo.InvariantCulture, Resources.MissingFilesDialog_UI_Filter_Title, fileItem.DisplayName),
+ Filter = Resources.MissingFilesDialog_UI_Filter_Logfiles,
+ FileName = fileItem.DisplayName,
+ CheckFileExists = true,
+ Multiselect = false
+ };
+
+ // Try to set initial directory from original path
+ try
+ {
+ var directory = Path.GetDirectoryName(fileItem.OriginalPath);
+ if (!string.IsNullOrEmpty(directory) && Directory.Exists(directory))
+ {
+ openFileDialog.InitialDirectory = directory;
+ }
+ }
+ catch (Exception ex) when (ex is ArgumentException or
+ PathTooLongException or
+ NotSupportedException or
+ UnauthorizedAccessException)
+ {
+ // Ignore if path is invalid
+ }
+
+ if (openFileDialog.ShowDialog(this) == DialogResult.OK)
+ {
+ // User selected a file
+ fileItem.SelectedPath = openFileDialog.FileName;
+ fileItem.Status = FileStatus.AlternativeSelected;
+
+ // Store the alternative
+ SelectedAlternatives[fileItem.OriginalPath] = fileItem.SelectedPath;
+
+ // Refresh the ListView
+ PopulateListView();
+ UpdateSummary();
+ }
+ }
+
+ ///
+ /// Determines the appropriate layout result based on radio button selection.
+ ///
+ /// The layout-related result
+ private MissingFilesDialogResult DetermineLayoutResult ()
+ {
+ // If layout options are not shown or there's no layout data, return LoadValidFiles
+ if (!_hasLayoutData || !panelLayoutOptions.Visible)
+ {
+ return MissingFilesDialogResult.LoadValidFiles;
+ }
+
+ // Determine layout-related result
+ if (radioButtonCloseTabs.Checked)
+ {
+ return MissingFilesDialogResult.CloseTabsAndRestoreLayout;
+ }
+ else if (radioButtonNewWindow.Checked)
+ {
+ return MissingFilesDialogResult.OpenInNewWindow;
+ }
+ else if (radioButtonIgnoreLayout.Checked)
+ {
+ return MissingFilesDialogResult.IgnoreLayout;
+ }
+
+ // Default to LoadValidFiles
+ return MissingFilesDialogResult.LoadValidFiles;
+ }
+
+ #endregion
+
+ #region Event Handlers
+
+ private void OnListViewSelectedIndexChanged (object sender, EventArgs e)
+ {
+ if (listViewFiles.SelectedItems.Count > 0)
+ {
+ var selectedItem = listViewFiles.SelectedItems[0];
+ var fileItem = selectedItem.Tag as MissingFileItem;
+
+ // Enable browse button for any file that is not valid (allow browsing/re-browsing)
+ buttonBrowse.Enabled = fileItem?.Status is
+ FileStatus.Missing or
+ FileStatus.MissingWithAlternatives or
+ FileStatus.AlternativeSelected;
+ }
+ else
+ {
+ buttonBrowse.Enabled = false;
+ }
+ }
+
+ private void OnListViewDoubleClick (object sender, EventArgs e)
+ {
+ // Double-click to browse for missing file
+ if (listViewFiles.SelectedItems.Count > 0)
+ {
+ var selectedItem = listViewFiles.SelectedItems[0];
+ var fileItem = selectedItem.Tag as MissingFileItem;
+
+ if (fileItem?.Status is
+ FileStatus.Missing or
+ FileStatus.MissingWithAlternatives or
+ FileStatus.AlternativeSelected)
+ {
+ BrowseForFile(fileItem);
+ }
+ }
+ }
+
+ private void OnButtonBrowseClick (object sender, EventArgs e)
+ {
+ if (listViewFiles.SelectedItems.Count > 0)
+ {
+ var selectedItem = listViewFiles.SelectedItems[0];
+
+ if (selectedItem.Tag is MissingFileItem fileItem)
+ {
+ BrowseForFile(fileItem);
+ }
+ }
+ }
+
+ private void OnButtonLoadClick (object sender, EventArgs e)
+ {
+ Result = DetermineLayoutResult();
+ UpdateSessionFile = false;
+ DialogResult = DialogResult.OK;
+ Close();
+ }
+
+ private void OnButtonLoadAndUpdateClick (object sender, EventArgs e)
+ {
+ Result = DetermineLayoutResult();
+ UpdateSessionFile = true;
+ DialogResult = DialogResult.OK;
+ Close();
+ }
+
+ private void OnButtonCancelClick (object sender, EventArgs e)
+ {
+ Result = MissingFilesDialogResult.Cancel;
+ UpdateSessionFile = false;
+ DialogResult = DialogResult.Cancel;
+ Close();
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx b/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx
new file mode 100644
index 00000000..1af7de15
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFilesDialog.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs b/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs
new file mode 100644
index 00000000..d0ada4ec
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFilesDialogResult.cs
@@ -0,0 +1,55 @@
+namespace LogExpert.UI.Dialogs;
+
+///
+/// Represents the result of the Missing Files Dialog interaction.
+///
+public enum MissingFilesDialogResult
+{
+ ///
+ /// User cancelled the operation.
+ ///
+ Cancel,
+
+ ///
+ /// Load only the valid files that were found.
+ ///
+ LoadValidFiles,
+
+ ///
+ /// Load valid files and update the session file with new paths.
+ ///
+ LoadAndUpdateSession,
+
+ ///
+ /// Close existing tabs before loading the project with layout restoration.
+ /// Used when there are existing tabs open and the project has layout data.
+ ///
+ CloseTabsAndRestoreLayout,
+
+ ///
+ /// Open the project in a new window.
+ /// Used when there are existing tabs open and the project has layout data.
+ ///
+ OpenInNewWindow,
+
+ ///
+ /// Ignore the layout data and just load the files.
+ /// Used when there are existing tabs open and the project has layout data.
+ ///
+ IgnoreLayout,
+
+ ///
+ /// Show a message box with information about the missing files.
+ ///
+ ShowMissingFilesMessage,
+
+ ///
+ /// Retry loading the files after resolving the issues.
+ ///
+ RetryLoadFiles,
+
+ ///
+ /// Skip the missing files and continue with the operation.
+ ///
+ SkipMissingFiles
+}
diff --git a/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs b/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs
new file mode 100644
index 00000000..da39de1f
--- /dev/null
+++ b/src/LogExpert.UI/Dialogs/MissingFilesMessageBox.cs
@@ -0,0 +1,56 @@
+using System.Runtime.Versioning;
+using System.Text;
+
+using LogExpert.Core.Classes.Persister;
+
+namespace LogExpert.UI.Dialogs;
+
+///
+/// Temporary helper for showing missing file alerts until full dialog is implemented.
+/// This provides a simple MessageBox-based notification system for Phase 1 of the implementation.
+///
+[SupportedOSPlatform("windows")]
+internal static class MissingFilesMessageBox
+{
+ ///
+ /// Shows a message box alerting the user about missing files from a project/session.
+ ///
+ /// The validation result containing missing file information
+ /// True if user wants to continue loading valid files, false to cancel
+ public static bool Show (ProjectValidationResult validationResult)
+ {
+ ArgumentNullException.ThrowIfNull(validationResult);
+
+ var sb = new StringBuilder();
+ _ = sb.AppendLine("Some files from the session could not be found:");
+ _ = sb.AppendLine();
+
+ // Show first 10 missing files
+ var displayCount = Math.Min(10, validationResult.MissingFiles.Count);
+ for (var i = 0; i < displayCount; i++)
+ {
+ var missing = validationResult.MissingFiles[i];
+ _ = sb.AppendLine($" • {Path.GetFileName(missing)}");
+ }
+
+ // If there are more than 10, show count of remaining
+ if (validationResult.MissingFiles.Count > 10)
+ {
+ _ = sb.AppendLine($" ... and {validationResult.MissingFiles.Count - 10} more");
+ }
+
+ _ = sb.AppendLine();
+ var totalFiles = validationResult.ValidFiles.Count + validationResult.MissingFiles.Count;
+ _ = sb.AppendLine($"Found: {validationResult.ValidFiles.Count} of {totalFiles} files");
+ _ = sb.AppendLine();
+ _ = sb.AppendLine("Do you want to load the files that were found?");
+
+ var result = MessageBox.Show(
+ sb.ToString(),
+ "Missing Files",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning);
+
+ return result == DialogResult.Yes;
+ }
+}
diff --git a/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs b/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs
index 304b2e3d..5b0d78ff 100644
--- a/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs
+++ b/src/LogExpert.UI/Dialogs/ProjectLoadDlg.cs
@@ -30,12 +30,12 @@ public ProjectLoadDlg ()
private void ApplyResources ()
{
- Text = Resources.ProjectLoadDlg_UI_Title;
- labelInformational.Text = Resources.ProjectLoadDlg_UI_Label_Informational;
- labelChooseHowToProceed.Text = Resources.ProjectLoadDlg_UI_Label_ChooseHowToProceed;
- buttonCloseTabs.Text = Resources.ProjectLoadDlg_UI_Button_CloseTabs;
- buttonNewWindow.Text = Resources.ProjectLoadDlg_UI_Button_NewWindow;
- buttonIgnore.Text = Resources.ProjectLoadDlg_UI_Button_Ignore;
+ Text = Resources.MissingFilesDialog_UI_Title;
+ labelInformational.Text = Resources.MissingFilesDialog_UI_Label_Informational;
+ labelChooseHowToProceed.Text = Resources.MissingFilesDialog_UI_Label_ChooseHowToProceed;
+ buttonCloseTabs.Text = Resources.MissingFilesDialog_UI_Button_CloseTabs;
+ buttonNewWindow.Text = Resources.MissingFilesDialog_UI_Button_NewWindow;
+ buttonIgnore.Text = Resources.MissingFilesDialog_UI_Button_Ignore;
}
#endregion
diff --git a/src/PluginRegistry/FileSystem/LocalFileSystem.cs b/src/PluginRegistry/FileSystem/LocalFileSystem.cs
index c684bb87..262f33df 100644
--- a/src/PluginRegistry/FileSystem/LocalFileSystem.cs
+++ b/src/PluginRegistry/FileSystem/LocalFileSystem.cs
@@ -21,6 +21,12 @@ ArgumentNullException or
}
}
+ ///
+ /// Retrieves information about a log file specified by a file URI.
+ ///
+ /// The URI string that identifies the log file. Must be a valid file URI.
+ /// An object that provides information about the specified log file.
+ /// Thrown if the provided URI string does not represent a file URI.
public ILogFileInfo GetLogfileInfo (string uriString)
{
Uri uri = new(uriString);
diff --git a/src/PluginRegistry/FileSystem/LogFileInfo.cs b/src/PluginRegistry/FileSystem/LogFileInfo.cs
index ef7e12c5..bfcdbe00 100644
--- a/src/PluginRegistry/FileSystem/LogFileInfo.cs
+++ b/src/PluginRegistry/FileSystem/LogFileInfo.cs
@@ -10,7 +10,7 @@ public class LogFileInfo : ILogFileInfo
private const int RETRY_COUNT = 5;
private const int RETRY_SLEEP = 250;
- private static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
+ private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
//FileStream fStream;
private readonly FileInfo fInfo;
@@ -21,7 +21,7 @@ public class LogFileInfo : ILogFileInfo
#region cTor
- public LogFileInfo(Uri fileUri)
+ public LogFileInfo (Uri fileUri)
{
fInfo = new FileInfo(fileUri.LocalPath);
Uri = fileUri;
@@ -37,7 +37,6 @@ public LogFileInfo(Uri fileUri)
public string FileName => fInfo.Name;
-
public string DirectoryName => fInfo.DirectoryName;
public char DirectorySeparatorChar => Path.DirectorySeparatorChar;
@@ -124,7 +123,7 @@ public long LengthWithoutRetry
/// rollover situations.
///
///
- public Stream OpenStream()
+ public Stream OpenStream ()
{
var retry = RETRY_COUNT;
@@ -158,7 +157,7 @@ public Stream OpenStream()
}
//TODO Replace with Event from FileSystemWatcher
- public bool FileHasChanged()
+ public bool FileHasChanged ()
{
if (LengthWithoutRetry != lastLength)
{
@@ -169,7 +168,7 @@ public bool FileHasChanged()
return false;
}
- public override string ToString()
+ public override string ToString ()
{
return fInfo.FullName + ", OldLen: " + OriginalLength + ", Len: " + Length;
}
diff --git a/src/PluginRegistry/PluginHashGenerator.Generated.cs b/src/PluginRegistry/PluginHashGenerator.Generated.cs
index e5d5109a..7a878dc0 100644
--- a/src/PluginRegistry/PluginHashGenerator.Generated.cs
+++ b/src/PluginRegistry/PluginHashGenerator.Generated.cs
@@ -10,7 +10,7 @@ public static partial class PluginValidator
{
///
/// Gets pre-calculated SHA256 hashes for built-in plugins.
- /// Generated: 2025-12-12 22:08:58 UTC
+ /// Generated: 2025-12-16 15:51:20 UTC
/// Configuration: Release
/// Plugin count: 22
///
@@ -18,28 +18,28 @@ public static Dictionary GetBuiltInPluginHashes()
{
return new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- ["AutoColumnizer.dll"] = "9BBF1F553FD9E5A9F669F107232985F48D93C6D132069464700201247CEDE5DD",
+ ["AutoColumnizer.dll"] = "FBFCB4C9FEBF8DA0DDB1822BDF7CFCADF1185574917D8705C597864D1ACBFE0C",
["BouncyCastle.Cryptography.dll"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
["BouncyCastle.Cryptography.dll (x86)"] = "E5EEAF6D263C493619982FD3638E6135077311D08C961E1FE128F9107D29EBC6",
- ["CsvColumnizer.dll"] = "B67093810D0144BAC80E70F981590AEF5E341C13852F151633345B5D1C4A33E5",
- ["CsvColumnizer.dll (x86)"] = "B67093810D0144BAC80E70F981590AEF5E341C13852F151633345B5D1C4A33E5",
- ["DefaultPlugins.dll"] = "76ABF37B8C9DD574EE6D9C42860D4D60818EA0407B6B3EBB03F31B33C8DCC50A",
- ["FlashIconHighlighter.dll"] = "CDDD76BC56EAFDB6D5003E3EFC80122F4D20BE05F59F71FB094127C5FE02D700",
- ["GlassfishColumnizer.dll"] = "198BECCD93612FC5041EE37E5E69E2352AC7A18416EAA7E205FB99133AB8261C",
- ["JsonColumnizer.dll"] = "9E801A0C414CF6512087730906CDD9759908CD78CAB547B55C4DE16727B10922",
- ["JsonCompactColumnizer.dll"] = "70EABBEA5CA5B32255CFB02C87B6512CE9C3B20B21F444DC5716E3F8A7512FD0",
- ["Log4jXmlColumnizer.dll"] = "6F64839262E7DBEF08000812D84FC591965835B74EF8551C022E827070A136A0",
- ["LogExpert.Core.dll"] = "F07BD482E92D8E17669C71F020BD392979177BE8E4F43AE3F4EC544411EB849E",
- ["LogExpert.Resources.dll"] = "ED0E7ABC183982C10D7F48ECEA12B3AA4F2D518298E6BFB168F2E0921EF063DB",
+ ["CsvColumnizer.dll"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982",
+ ["CsvColumnizer.dll (x86)"] = "672B48664FDBDB7E71A01AEE6704D34544257B6A977E01EC0A514FAEE030E982",
+ ["DefaultPlugins.dll"] = "53FA9690E55C7A1C90601CCCCA4FFC808BB783DFAA1FDF42EB924DF3CDB65D40",
+ ["FlashIconHighlighter.dll"] = "AC101CC62538DB565C462D5A9A6EAA57D85AEA4954FAAEF271FEAC7F98FD1CB1",
+ ["GlassfishColumnizer.dll"] = "915BDD327C531973F3355BC5B24AE70B0EA618269E9367DA4FA919B4BA08D171",
+ ["JsonColumnizer.dll"] = "8A8B8021A4D146424947473A5FC2F0C07248BD4FAC079C00E586ADAA153CA9ED",
+ ["JsonCompactColumnizer.dll"] = "6B26775A81C6FC4232C08BBAFDCE35A0EFA80A367FC00271DF00E89AF2F74802",
+ ["Log4jXmlColumnizer.dll"] = "B645973F639B23A51D066DC283A92E3BDAF058934C6C6378B6F13247B75077CB",
+ ["LogExpert.Core.dll"] = "A370D1AEF2A790D16622CD15C67270799A87555996A7A0D0A4D98B950B9C3CB7",
+ ["LogExpert.Resources.dll"] = "1D761591625ED49FD0DE29ABDB2800D865BD99B359397CF7790004B3CC9E6738",
["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"] = "A4C92D2E70491F3D65CDA46106F08277562950800FB0DE8230201CE129F5FB5C",
- ["SftpFileSystem.dll"] = "D9643E14F3CF02F849BA12618D6F092ABEA07724ED9837F858AD88317E28B4C8",
- ["SftpFileSystem.dll (x86)"] = "E1B167E38290E50E5EDC0754804792425363DA8EC7C38645C7AFA1ECB7118157",
- ["SftpFileSystem.Resources.dll"] = "445FEAD03A0B535813A737A41B0C62C9E9341578FD3EACA3309766683B774561",
- ["SftpFileSystem.Resources.dll (x86)"] = "445FEAD03A0B535813A737A41B0C62C9E9341578FD3EACA3309766683B774561",
+ ["RegexColumnizer.dll"] = "DD44507671520B3E2CD93311A651EFC7632C757FDA85750470A6FFCB74F1AB65",
+ ["SftpFileSystem.dll"] = "16818A6B8B7178372CCB1CD6ED8C4B0542FDD7D10C8C9AAE76704F0ED2CC4362",
+ ["SftpFileSystem.dll (x86)"] = "94B725554D2BBF79BBB0B4776C01AE0D1E8F06C158F5D234C2D94F5E62A900BC",
+ ["SftpFileSystem.Resources.dll"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4",
+ ["SftpFileSystem.Resources.dll (x86)"] = "FEFD8C274032A70FDB03EA8363D7F877BEBAF2C454238820D6D17ECEF79FC8A4",
};
}