sliding window for vision builder image strip pannel
This commit is contained in:
426
Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
Normal file
426
Hawkeye.VisionBuilder/Features/ImagesStrip/ImagesViewModel.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user