Some selections.

This commit is contained in:
Burak Kaan Köse
2025-11-06 23:54:32 +01:00
parent 5f9b51e4db
commit 175ed24a66
2 changed files with 118 additions and 60 deletions
@@ -1008,7 +1008,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
}
public Task SelectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = true, true);
public Task UnselectAllAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = false, true);
public Task UnselectAllAsync(IMailListItem exceptItem = null) => ExecuteWithoutRaiseSelectionChangedAsync(a => a.IsSelected = false && a != exceptItem, true);
public Task CollapseAllThreadsAsync() => ExecuteWithoutRaiseSelectionChangedAsync(a => { if (a is ThreadMailItemViewModel thread) thread.IsThreadExpanded = false; }, true);
private Task ExecuteUIThread(Action action) => CoreDispatcher?.ExecuteOnUIThread(action);
+117 -59
View File
@@ -558,90 +558,148 @@ public sealed partial class MailListPage : MailListPageAbstract,
{
if (clickedItem == null) return;
bool isSelectedItemFromThread = listView.IsThreadListView;
// Requirements (summary):
// CTRL pressed -> multi-select behaviour
// * Clicking single item toggles only that item.
// * Clicking thread header toggles selection of thread AND all its children (all on or all off).
// * Clicking an item inside a thread toggles only that child item.
// CTRL NOT pressed -> single-select (exclusive) with toggle support (can leave zero selected)
// * Clicking thread header: unselect everything else, collapse all other threads, select only the thread + first child.
// If already in that state (thread selected and first child selected), clicking again unselects all (nothing selected).
// * Clicking a single (non-thread) item OR a child item: collapse & unselect all others then toggle that item's selection.
// If it was selected, result is nothing selected.
bool isCtrlPressed = KeyPressService.IsCtrlKeyPressed();
bool isClickingThreadItem = clickedItem is ThreadMailItemViewModel;
// Unselect all items. It's single selection.
if (!isCtrlPressed)
// Helper local to collapse all other threads (we always collapse ALL then possibly re-expand the active thread per rules)
async Task CollapseAllThreadsExceptAsync(ThreadMailItemViewModel? except)
{
await ViewModel.MailCollection.UnselectAllAsync();
if (!isSelectedItemFromThread && !isClickingThreadItem)
await ViewModel.MailCollection.CollapseAllThreadsAsync();
if (except != null)
{
await ViewModel.MailCollection.CollapseAllThreadsAsync();
// We'll expand explicitly when required by logic below.
}
}
if (clickedItem is MailItemViewModel mailListItem)
if (isCtrlPressed)
{
mailListItem.IsSelected = !mailListItem.IsSelected;
}
else if (clickedItem is ThreadMailItemViewModel threadMailItemViewModel)
{
// Extended selection mode handling for threads
if (isCtrlPressed)
switch (clickedItem)
{
// If thread is selected and Ctrl is pressed
if (threadMailItemViewModel.IsSelected)
{
// If thread was collapsed, expand it
if (!threadMailItemViewModel.IsThreadExpanded)
case ThreadMailItemViewModel thread:
{
threadMailItemViewModel.IsThreadExpanded = true;
}
else
{
// Check if all items are selected.
// If so, then unselect all items in the thread and unselect the thread itself.
if (threadMailItemViewModel.ThreadEmails.All(a => a.IsSelected))
// Determine if thread + all children currently selected
bool allSelected = thread.IsSelected && thread.ThreadEmails.All(e => e.IsSelected);
if (allSelected)
{
foreach (var threadEmail in threadMailItemViewModel.ThreadEmails)
{
threadEmail.IsSelected = false;
}
threadMailItemViewModel.IsSelected = false;
return;
// Unselect thread & all children
thread.IsSelected = false;
foreach (var child in thread.ThreadEmails)
child.IsSelected = false;
}
else
{
// If thread was already expanded, select all items in the thread
foreach (var threadEmail in threadMailItemViewModel.ThreadEmails)
{
threadEmail.IsSelected = true;
}
// Select thread & all children (do NOT disturb other selections in CTRL mode)
thread.IsSelected = true;
foreach (var child in thread.ThreadEmails)
child.IsSelected = true;
// Keep it expanded so user can see items
thread.IsThreadExpanded = true;
}
break;
}
}
else
{
// Thread is not selected, select and expand it.
if (!threadMailItemViewModel.IsThreadExpanded) threadMailItemViewModel.IsThreadExpanded = true;
if (!threadMailItemViewModel.IsSelected)
case MailItemViewModel mail:
{
threadMailItemViewModel.IsSelected = true;
// Toggle just this item; no collapse/unselect of others in multi-select mode.
mail.IsSelected = !mail.IsSelected;
break;
}
}
return; // Multi-select path ends here.
}
foreach (var threadEmail in threadMailItemViewModel.ThreadEmails)
{
threadEmail.IsSelected = true;
}
// SINGLE-SELECTION (exclusive) MODE WITH TOGGLE SUPPORT
if (clickedItem is ThreadMailItemViewModel clickedThread)
{
bool wasThreadSelected = clickedThread.IsSelected;
// Reset everything first (exclusive selection scenario)
await ViewModel.MailCollection.UnselectAllAsync();
await CollapseAllThreadsExceptAsync(clickedThread);
if (wasThreadSelected)
{
// Toggle off -> leave nothing selected (all unselected, thread collapsed)
clickedThread.IsThreadExpanded = false;
return;
}
// Select thread + first child only
clickedThread.IsSelected = true;
var firstChild = clickedThread.ThreadEmails.FirstOrDefault();
if (firstChild != null)
{
// Ensure only first child selected
foreach (var child in clickedThread.ThreadEmails)
{
child.IsSelected = child == firstChild;
}
}
clickedThread.IsThreadExpanded = true; // Show contents of active thread
}
else if (clickedItem is MailItemViewModel clickedMail)
{
bool wasSelected = clickedMail.IsSelected;
// Determine if this mail belongs to an already selected & expanded thread.
// If so, we only want to switch the selection inside that thread without collapsing or unselecting the thread header.
ThreadMailItemViewModel? parentThread = null;
foreach (var group in ViewModel.MailCollection.MailItems)
{
foreach (var item in group)
{
if (item is ThreadMailItemViewModel thread && thread.ThreadEmails.Contains(clickedMail))
{
parentThread = thread;
break;
}
}
if (parentThread != null) break;
}
else
{
// No Ctrl pressed, toggle expansion state (default behavior)
threadMailItemViewModel.IsThreadExpanded = !threadMailItemViewModel.IsThreadExpanded;
// Select the first item in the thread if none is selected
if (!threadMailItemViewModel.IsSelected)
bool isInSelectedExpandedThread = parentThread != null && parentThread.IsSelected && parentThread.IsThreadExpanded;
if (isInSelectedExpandedThread)
{
// Switch selection within the thread: unselect previously selected children, select the clicked one.
if (parentThread?.ThreadEmails != null)
{
threadMailItemViewModel.IsSelected = true;
var firstEmail = threadMailItemViewModel.ThreadEmails.FirstOrDefault();
firstEmail?.IsSelected = true;
foreach (var child in parentThread.ThreadEmails)
{
child.IsSelected = child == clickedMail && !wasSelected; // If clicking an already selected child -> toggle off (none selected in thread except header)
}
}
if (wasSelected && parentThread != null)
{
// Clicking the already selected child should leave the thread header selected (canonical state: thread + first child previously).
// Decide whether to keep a child selected; spec wants toggle off allowed, so leave no child selected.
// Ensure parent thread stays selected & expanded.
parentThread.IsSelected = true;
parentThread.IsThreadExpanded = true;
}
return; // Done.
}
// Normal single-item (non-thread or entering a thread via child) behavior.
await ViewModel.MailCollection.UnselectAllAsync();
await ViewModel.MailCollection.CollapseAllThreadsAsync();
if (!wasSelected)
{
clickedMail.IsSelected = true; // Toggle on
}
// else leave all unselected (toggle off)
}
}