426 lines
13 KiB
C#
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;
|
|
}
|
|
} |