From dffc1b7750043c004de01fead5b61ff7b997011c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Polykanine?= Date: Tue, 8 Sep 2026 22:18:02 +0200 Subject: [PATCH] Let the image list select more than one row Convert Selected against a single-select list was a contradiction, and the code had noticed before anyone did: ConvertSelectedMenuItem_Click already collected every selected index and handed the whole list to ConvertItemsAsync. It had been converting exactly one image for as long as it existed, because the control could not offer it a second. So the list is multi-select now, and the handlers that were written for one row are told which row they mean. Remove takes every selected image rather than the first, walking the indices in descending order so removing one cannot shift another still to go, and afterwards lands the selection where the first removed row used to be. A list with nothing selected announces nothing when focus returns to it, so ending up somewhere is not cosmetic. Create Multi-size ICO and Fit to File Size both make one output from one image, so they act on the focused row rather than silently taking the topmost of a multiple selection - which would work on an image the user is not looking at. The preview follows the focused row too, for the same reason: extending a selection downward should show where you are rather than where you started. Verified at the window level rather than through the property: the list window carries no LVS_SINGLESEL, and selecting two rows yields two. Screen readers announce multiple selection well - extending, moving the focus without extending, and selecting everything at once all come across on JAWS, NVDA and Narrator. The one gap is that none of them names the mode for a list view the way they say "multi select list box", which appears to be decided by control class rather than by anything the control reports. Claude-Session: https://claude.ai/code/session_01TJ8i7jHmkUjVCjVccLp6Hf --- src/Sic/MainWindow.Designer.cs | 2 +- src/Sic/MainWindow.cs | 44 +++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Sic/MainWindow.Designer.cs b/src/Sic/MainWindow.Designer.cs index 0740829..c427573 100644 --- a/src/Sic/MainWindow.Designer.cs +++ b/src/Sic/MainWindow.Designer.cs @@ -119,7 +119,7 @@ private void InitializeComponent() { } imageListView.Dock = DockStyle.Fill; - imageListView.MultiSelect = false; + imageListView.MultiSelect = true; imageListView.Name = "imageListView"; imageListView.AccessibleName = "Images list"; imageListView.AllowDrop = true; diff --git a/src/Sic/MainWindow.cs b/src/Sic/MainWindow.cs index 4e7ff64..9bf1d9d 100644 --- a/src/Sic/MainWindow.cs +++ b/src/Sic/MainWindow.cs @@ -401,13 +401,34 @@ private async void AddByLinkMenuItem_Click(object? sender, EventArgs e) { await PasteFromUrlAsync(dialog.Url); } + /// + /// The row a single-image command should act on: the one carrying the focus rectangle when + /// it is selected, and otherwise the first selected row. + /// + private int FocusedOrFirstSelectedIndex() => + imageListView.FocusedItem is { Selected: true } focused + ? focused.Index + : imageListView.SelectedItems[0].Index; + private void RemoveMenuItem_Click(object? sender, EventArgs e) { if (_imageItems.Count == 0 || imageListView.SelectedItems.Count == 0) return; - var index = imageListView.SelectedItems[0].Index; - _imageItems.RemoveAt(index); - imageListView.Items.RemoveAt(index); + // Descending, so removing one cannot shift the index of another still to go. + var indices = imageListView.SelectedItems + .Select(row => row.Index) + .OrderByDescending(i => i) + .ToList(); + + foreach (var removed in indices) { + _imageItems.RemoveAt(removed); + imageListView.Items.RemoveAt(removed); + } + + // Land on the row that took the place of the first one removed, so the selection ends + // up somewhere predictable rather than nowhere - a list with nothing selected announces + // nothing when focus returns to it. + var index = indices[^1]; if (imageListView.Items.Count > 0) { var newIndex = index < imageListView.Items.Count ? index : imageListView.Items.Count - 1; @@ -703,6 +724,11 @@ private async void ConvertSelectedMenuItem_Click(object? sender, EventArgs e) { await ConvertItemsAsync(selectedIndices); } + /// + /// One image in, one .ico out, so this works on the focused row even when several are + /// selected - taking the topmost of a multiple selection would act on an image the user is + /// not looking at. + /// private async void CreateMultiSizeIcoMenuItem_Click(object? sender, EventArgs e) { if (_imageItems.Count == 0 || imageListView.SelectedItems.Count == 0) return; @@ -712,7 +738,7 @@ private async void CreateMultiSizeIcoMenuItem_Click(object? sender, EventArgs e) return; var sizes = presetDialog.SelectedSizes; - var index = imageListView.SelectedItems[0].Index; + var index = FocusedOrFirstSelectedIndex(); var item = _imageItems[index]; var outputFolder = ValidateOutputFolder(); var outputPath = ImageConverter.GenerateOutputPath(item, "ICO", outputFolder, Config.General.SaveToSourceFolder); @@ -789,7 +815,7 @@ private async void FitToFileSizeMenuItem_Click(object? sender, EventArgs e) { var maxBytes = fitDialog.MaxBytes; var maxWidth = fitDialog.MaxWidth; - var index = imageListView.SelectedItems[0].Index; + var index = FocusedOrFirstSelectedIndex(); var item = _imageItems[index]; var formats = ImageConverter.GetSizeFitFormats(ImageConverter.GetEnabledFormats(Config.General.GetEnabledFormatKeys())); @@ -1119,7 +1145,13 @@ private void ImageListView_SelectedIndexChanged(object? sender, EventArgs e) { return; } - var index = imageListView.SelectedItems[0].Index; + // The focused row when it is part of the selection, otherwise the first selected one. + // Extending a selection moves the focus, and previewing where the user actually is + // beats previewing where they started. + var index = imageListView.FocusedItem is { Selected: true } focused + ? focused.Index + : imageListView.SelectedItems[0].Index; + var item = _imageItems[index]; _selectedItem = item;