Files
HawkeyeVision/Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs

426 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace Hawkeye.VisionBuilder.Features.ImagesStrip;
public class ImagesViewModel
{
private const int DefaultBufferSize = 10;
private string? _directoryPath;
private List<string> _allImagePaths = new();
private int _currentIndex = -1;
private int _bufferSize;
private HashSet<int> _currentLoadedIndices = new();
public ObservableCollection<ImageItem> LoadedImages { get; } = new();
public int BufferSize
{
get => _bufferSize;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Buffer size must be non-negative.");
_bufferSize = value;
if (_currentIndex >= 0)
LoadWindow();
}
}
public int CurrentIndex => _currentIndex;
public int CurrentRelativeIndex => _currentIndex - (_currentIndex - _bufferSize < 0 ? 0 : _currentIndex - _bufferSize);
public int CurrentIndexOnLoaded => GetIndexOfLoaded();
public int TotalCount => _allImagePaths.Count;
public string? CurrentImagePath => _currentIndex >= 0 && _currentIndex < _allImagePaths.Count
? _allImagePaths[_currentIndex]
: null;
public bool CanGoNext => _currentIndex < _allImagePaths.Count - 1;
public bool CanGoPrevious => _currentIndex > 0;
public bool HasImages => _allImagePaths.Count > 0;
// Events
public event EventHandler<ImageAddedEventArgs[]>? ImagesAdded;
public event EventHandler<ImageRemovedEventArgs[]>? ImagesRemoved;
public event EventHandler? ImagesCleared;
public event EventHandler<CurrentImageChangedEventArgs>? CurrentImageChanged;
public event EventHandler<ImagesLoadedEventArgs>? ImagesLoaded;
public event EventHandler? WindowRefreshed;
public ImagesViewModel() : this(DefaultBufferSize)
{
}
public ImagesViewModel(int bufferSize)
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize), "Buffer size must be non-negative.");
_bufferSize = bufferSize;
}
private int GetIndexOfLoaded()
{
return LoadedImages.FirstOrDefault(img => img.IsCurrent)?.Index ?? -1;
}
public int GetIndexFromPath(string path)
{
return _allImagePaths.IndexOf(path);
}
/// <summary>
/// Loads all image files from the specified directory path.
/// </summary>
/// <param name="directoryPath">The directory path containing images</param>
/// <param name="searchPattern">Optional search pattern (default: *.* for all files)</param>
/// <param name="supportedExtensions">Optional list of supported extensions (e.g., .jpg, .png, .bmp)</param>
public void LoadFromDirectory(string directoryPath, string searchPattern = "*.*", string[]? supportedExtensions = null)
{
if (string.IsNullOrWhiteSpace(directoryPath))
throw new ArgumentException("Directory path cannot be null or empty.", nameof(directoryPath));
if (!Directory.Exists(directoryPath))
throw new DirectoryNotFoundException($"Directory not found: {directoryPath}");
_directoryPath = directoryPath;
var defaultExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tif", ".tiff" };
var extensions = supportedExtensions ?? defaultExtensions;
var files = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories)
.Where(f => !Path.GetFileNameWithoutExtension(f).Contains("_analysis") && extensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
.OrderBy(f => f)
.ToList();
_allImagePaths = files;
_currentIndex = files.Count > 0 ? 0 : -1;
LoadWindow();
OnImagesLoaded(new ImagesLoadedEventArgs(files.Count, _directoryPath));
OnCurrentImageChanged(new CurrentImageChangedEventArgs(CurrentIndex,CurrentIndex,CurrentImagePath));
}
/// <summary>
/// Navigate to the first image.
/// </summary>
public bool First()
{
if (_allImagePaths.Count == 0)
return false;
if (_currentIndex == 0)
return false;
var oldIndex = _currentIndex;
_currentIndex = 0;
LoadWindow();
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
return true;
}
/// <summary>
/// Navigate to the previous image.
/// </summary>
public bool Previous()
{
if (!CanGoPrevious)
return false;
var oldIndex = _currentIndex;
_currentIndex--;
LoadWindow();
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
return true;
}
/// <summary>
/// Navigate to the next image.
/// </summary>
public bool Next()
{
if (!CanGoNext)
return false;
var oldIndex = _currentIndex;
_currentIndex++;
LoadWindow();
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
return true;
}
/// <summary>
/// Navigate to the last image.
/// </summary>
public bool Last()
{
if (_allImagePaths.Count == 0)
return false;
var lastIndex = _allImagePaths.Count - 1;
if (_currentIndex == lastIndex)
return false;
var oldIndex = _currentIndex;
_currentIndex = lastIndex;
LoadWindow();
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
return true;
}
/// <summary>
/// Navigate to the Nth image (0-based index).
/// </summary>
public bool SelectNth(int index)
{
if (index < 0 || index >= _allImagePaths.Count)
throw new ArgumentOutOfRangeException(nameof(index), $"Index must be between 0 and {_allImagePaths.Count - 1}.");
var oldIndex = _currentIndex;
_currentIndex = index;
LoadWindow();
OnCurrentImageChanged(new CurrentImageChangedEventArgs(oldIndex, _currentIndex, CurrentImagePath));
return true;
}
public bool SelectByPath(string path)
{
var index = _allImagePaths.IndexOf(path);
if (index < 0)
return false;
return SelectNth(index);
}
/// <summary>
/// Clears all loaded images and resets the state.
/// </summary>
public void Clear()
{
// Fire remove events for all currently loaded images
OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
_allImagePaths.Clear();
LoadedImages.Clear();
_currentLoadedIndices.Clear();
_currentIndex = -1;
_directoryPath = null;
OnImagesCleared();
}
/// <summary>
/// Refreshes the current window by reloading images from disk.
/// </summary>
public void RefreshWindow()
{
LoadWindow();
OnWindowRefreshed();
}
/// <summary>
/// Reloads the directory, useful when files have been added or removed.
/// </summary>
public void ReloadDirectory(bool preserveCurrentImage = true)
{
if (string.IsNullOrWhiteSpace(_directoryPath))
return;
string? currentPath = preserveCurrentImage ? CurrentImagePath : null;
LoadFromDirectory(_directoryPath);
if (preserveCurrentImage && currentPath != null)
{
var newIndex = _allImagePaths.IndexOf(currentPath);
if (newIndex >= 0)
{
_currentIndex = newIndex;
LoadWindow();
}
}
}
private void LoadWindow()
{
if (_currentIndex < 0 || _allImagePaths.Count == 0)
{
OnImagesRemoved(_currentLoadedIndices.OrderByDescending(i => i).Select(x => new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
LoadedImages.Clear();
_currentLoadedIndices.Clear();
return;
}
int startIndex = Math.Max(0, _currentIndex - _bufferSize);
int endIndex = Math.Min(_allImagePaths.Count - 1, _currentIndex + _bufferSize);
// Create a set of new indices that should be loaded
var newIndices = new HashSet<int>();
for (int i = startIndex; i <= endIndex; i++)
{
newIndices.Add(i);
}
// Find indices to remove (in current but not in new)
var indicesToRemove = _currentLoadedIndices.Except(newIndices).OrderByDescending(i => i).ToList();
// Find indices to add (in new but not in current)
var indicesToAdd = newIndices.Except(_currentLoadedIndices).OrderBy(i => i).ToList();
// Remove images that are no longer in the window
foreach (var index in indicesToRemove)
{
var item = LoadedImages.FirstOrDefault(img => img.Index == index);
if (item != null)
{
LoadedImages.Remove(item);
}
_currentLoadedIndices.Remove(index);
}
OnImagesRemoved(indicesToRemove.Select(x=>new ImageRemovedEventArgs(x, _allImagePaths[x])).ToArray());
// Add new images to the window
foreach (var index in indicesToAdd)
{
var item = new ImageItem(index, _allImagePaths[index], index == _currentIndex);
// Insert at the correct position to maintain order
int insertIndex = 0;
for (int i = 0; i < LoadedImages.Count; i++)
{
if (LoadedImages[i].Index < index)
insertIndex = i + 1;
else
break;
}
LoadedImages.Insert(insertIndex, item);
_currentLoadedIndices.Add(index);
}
OnImagesAdded(indicesToAdd.Select(x => new ImageAddedEventArgs(x, _allImagePaths[x])).ToArray());
// Update IsCurrent flag for all items
for (int i = 0; i < LoadedImages.Count; i++)
{
var currentItem = LoadedImages[i];
if (currentItem.IsCurrent != (currentItem.Index == _currentIndex))
{
// Replace with updated item
LoadedImages[i] = new ImageItem(currentItem.Index, currentItem.FilePath, currentItem.Index == _currentIndex);
}
}
}
protected virtual void OnImagesAdded(ImageAddedEventArgs[] e)
{
ImagesAdded?.Invoke(this, e);
}
protected virtual void OnImagesRemoved(ImageRemovedEventArgs[] e)
{
ImagesRemoved?.Invoke(this, e);
}
protected virtual void OnImagesCleared()
{
ImagesCleared?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnCurrentImageChanged(CurrentImageChangedEventArgs e)
{
CurrentImageChanged?.Invoke(this, e);
}
protected virtual void OnImagesLoaded(ImagesLoadedEventArgs e)
{
ImagesLoaded?.Invoke(this, e);
}
protected virtual void OnWindowRefreshed()
{
WindowRefreshed?.Invoke(this, EventArgs.Empty);
}
}
public class ImageItem
{
public int Index { get; }
public string FilePath { get; }
public string FileName { get; }
public bool IsCurrent { get; }
public ImageItem(int index, string filePath, bool isCurrent)
{
Index = index;
FilePath = filePath;
FileName = Path.GetFileName(filePath);
IsCurrent = isCurrent;
}
}
public class ImageAddedEventArgs : EventArgs
{
public int Index { get; }
public string FilePath { get; }
public ImageAddedEventArgs(int index, string filePath)
{
Index = index;
FilePath = filePath;
}
}
public class ImageRemovedEventArgs : EventArgs
{
public int Index { get; }
public string FilePath { get; }
public ImageRemovedEventArgs(int index, string filePath)
{
Index = index;
FilePath = filePath;
}
}
public class CurrentImageChangedEventArgs : EventArgs
{
public int OldIndex { get; }
public int NewIndex { get; }
public string? NewImagePath { get; }
public CurrentImageChangedEventArgs(int oldIndex, int newIndex, string? newImagePath)
{
OldIndex = oldIndex;
NewIndex = newIndex;
NewImagePath = newImagePath;
}
}
public class ImagesLoadedEventArgs : EventArgs
{
public int TotalCount { get; }
public string DirectoryPath { get; }
public ImagesLoadedEventArgs(int totalCount, string directoryPath)
{
TotalCount = totalCount;
DirectoryPath = directoryPath;
}
}