Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/UniGetUI/Controls/PackageWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ public void PackageItemContainer_DoubleTapped(object sender, DoubleTappedRoutedE
public void PackageItemContainer_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
=> _page.PackageItemContainer_PreviewKeyDown(sender, e);

public void PackageItemContainer_Tapped(object sender, TappedRoutedEventArgs e)
=> _page.PackageItemContainer_Tapped(sender, e);

public void PackageItemContainer_RightTapped(object sender, RightTappedRoutedEventArgs e)
=> _page.PackageItemContainer_RightTapped(sender, e);

Expand Down
12 changes: 9 additions & 3 deletions src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Package="{x:Bind Package}"
PreviewKeyDown="{x:Bind PackageItemContainer_PreviewKeyDown}"
RightTapped="{x:Bind PackageItemContainer_RightTapped}"
Tapped="{x:Bind PackageItemContainer_Tapped}"
Wrapper="{x:Bind Self}">

<Grid
Expand Down Expand Up @@ -210,6 +211,7 @@
Package="{x:Bind Package}"
PreviewKeyDown="{x:Bind PackageItemContainer_PreviewKeyDown}"
RightTapped="{x:Bind PackageItemContainer_RightTapped}"
Tapped="{x:Bind PackageItemContainer_Tapped}"
Wrapper="{x:Bind Self}">

<Grid
Expand Down Expand Up @@ -328,6 +330,7 @@
Package="{x:Bind Package}"
PreviewKeyDown="{x:Bind PackageItemContainer_PreviewKeyDown}"
RightTapped="{x:Bind PackageItemContainer_RightTapped}"
Tapped="{x:Bind PackageItemContainer_Tapped}"
Wrapper="{x:Bind Self}">

<Grid
Expand Down Expand Up @@ -1072,7 +1075,8 @@
CharacterReceived="PackageList_CharacterReceived"
ItemTemplate="{StaticResource PackageTemplate_List}"
ItemsSource="{x:Bind FilteredPackages}"
Layout="{StaticResource Layout_List}" />
Layout="{StaticResource Layout_List}"
SelectionMode="Extended" />
</Toolkit:Case>
<Toolkit:Case Value="1">
<ItemsView
Expand All @@ -1085,7 +1089,8 @@
CharacterReceived="PackageList_CharacterReceived"
ItemTemplate="{StaticResource PackageTemplate_Grid}"
ItemsSource="{x:Bind FilteredPackages}"
Layout="{StaticResource Layout_Grid}" />
Layout="{StaticResource Layout_Grid}"
SelectionMode="Extended" />
</Toolkit:Case>
<Toolkit:Case Value="2">
<ItemsView
Expand All @@ -1098,7 +1103,8 @@
CharacterReceived="PackageList_CharacterReceived"
ItemTemplate="{StaticResource PackageTemplate_Icons}"
ItemsSource="{x:Bind FilteredPackages}"
Layout="{StaticResource Layout_Icons}" />
Layout="{StaticResource Layout_Icons}"
SelectionMode="Extended" />
</Toolkit:Case>
</Toolkit:SwitchPresenter>

Expand Down
146 changes: 129 additions & 17 deletions src/UniGetUI/Pages/SoftwarePages/AbstractPackagesPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ protected string NoPackages_SubtitleText
private string TypeQuery = "";
private int LastKeyDown;
private readonly int QUERY_SEPARATION_TIME = 1000; // 500ms between keypresses starts a new query
private int _shiftSelectAnchorIndex = -1;
private int _lastNavigationIndex = -1;
private readonly HashSet<int> _selectedIndices = [];

private void ResetSelectionState()
{
_selectedIndices.Clear();
_shiftSelectAnchorIndex = -1;
_lastNavigationIndex = -1;
}

protected AbstractPackagesPage(PackagesPageData data)
{
Expand Down Expand Up @@ -592,12 +602,32 @@ protected async Task LoadPackages(ReloadReason reason)
}
}

private void SelectAndScrollTo(int index, bool focus)
private void SelectAndScrollTo(int index, bool focus, bool extendSelection = false)
{
if (index < 0 || index >= FilteredPackages.Count)
return;

CurrentPackageList.Select(index);
_selectedIndices.Clear();
if (extendSelection && _shiftSelectAnchorIndex >= 0 && _shiftSelectAnchorIndex < FilteredPackages.Count)
{
CurrentPackageList.DeselectAll();
int start = Math.Min(_shiftSelectAnchorIndex, index);
int end = Math.Max(_shiftSelectAnchorIndex, index);
for (int i = start; i <= end; i++)
{
CurrentPackageList.Select(i);
_selectedIndices.Add(i);
}
}
else
{
CurrentPackageList.DeselectAll();
CurrentPackageList.Select(index);
_shiftSelectAnchorIndex = index;
_selectedIndices.Add(index);
}

_lastNavigationIndex = index;

double position;
if (CurrentPackageList.Layout is StackLayout)
Expand Down Expand Up @@ -626,7 +656,10 @@ private void SelectAndScrollTo(int index, bool focus)
));
}

if (focus) Focus(FilteredPackages[index].Package);
// Skip Focus during extended selection — calling Focus(FocusState.Keyboard)
// on the new container triggers ItemsView's internal selection handling,
// which clobbers the range selection in Extended mode.
if (focus && !extendSelection) Focus(FilteredPackages[index].Package);
}

private void Focus(IPackage packageToFocus, int retryCount = 0)
Expand Down Expand Up @@ -799,6 +832,8 @@ protected void ApplyTextAndIconsToToolbar(
/// </summary>
public void FilterPackages(bool forceQueryUpdate = false)
{
ResetSelectionState();

var previousSelection = CurrentPackageList.SelectedItem as PackageWrapper;

List<IManagerSource> visibleSources = [];
Expand Down Expand Up @@ -965,13 +1000,15 @@ public void SortPackagesBy(ObservablePackageCollection.Sorter sorter)
if(sorter == FilteredPackages.CurrentSorter) FilteredPackages.Descending = !FilteredPackages.Descending;
FilteredPackages.SetSorter(sorter);
FilteredPackages.Sort();
ResetSelectionState();
UpdateSortingMenu();
}

public void SortPackagesBy(bool ascendent)
{
FilteredPackages.Descending = !ascendent;
FilteredPackages.Sort();
ResetSelectionState();
UpdateSortingMenu();
}

Expand Down Expand Up @@ -1106,20 +1143,70 @@ public void FocusPackageList()
=> CurrentPackageList.Focus(FocusState.Programmatic);


private void SelectSingleIndex(int index)
{
CurrentPackageList.DeselectAll();
CurrentPackageList.Select(index);
_shiftSelectAnchorIndex = index;
_lastNavigationIndex = index;
_selectedIndices.Clear();
_selectedIndices.Add(index);
}

public async Task ShowContextMenu(PackageWrapper wrapper)
{
CurrentPackageList.Select(wrapper.Index);
SelectSingleIndex(wrapper.Index);
await Task.Delay(20);
if(_lastContextMenuButtonTapped is not null)
(CurrentPackageList.ContextFlyout as BetterMenu)?.ShowAt(_lastContextMenuButtonTapped, new FlyoutShowOptions { Placement = FlyoutPlacementMode.RightEdgeAlignedTop });
WhenShowingContextMenu(wrapper.Package);
}

public void PackageItemContainer_Tapped(object sender, TappedRoutedEventArgs e)
{
if (sender is PackageItemContainer container && container.Package is not null)
{
int idx = container.Wrapper.Index;
bool isShiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift)
.HasFlag(CoreVirtualKeyStates.Down);
bool isCtrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control)
.HasFlag(CoreVirtualKeyStates.Down);

if (isCtrlPressed && !isShiftPressed)
{
// Ctrl+Click: toggle this item in/out of selection.
// ItemsView Extended mode handles the visual toggle natively.
if (!_selectedIndices.Remove(idx))
_selectedIndices.Add(idx);
}
else if (isShiftPressed)
{
if (_shiftSelectAnchorIndex < 0 || _shiftSelectAnchorIndex >= FilteredPackages.Count)
_shiftSelectAnchorIndex = idx;

// Shift+Click: select range from anchor to clicked item.
// ItemsView handles the visual range natively.
_selectedIndices.Clear();
int start = Math.Min(_shiftSelectAnchorIndex, idx);
int end = Math.Max(_shiftSelectAnchorIndex, idx);
for (int i = start; i <= end; i++)
_selectedIndices.Add(i);
}
else
{
// Plain click: always collapse to single selection.
// ItemsView Extended mode won't deselect the range when
// clicking an already-selected item, so force it here.
SelectSingleIndex(idx);
}
}
}

public void PackageItemContainer_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (sender is PackageItemContainer container && container.Package is not null)
{
CurrentPackageList.Select(container.Wrapper.Index);
SelectSingleIndex(container.Wrapper.Index);
container.Focus(FocusState.Keyboard);
WhenShowingContextMenu(container.Package);
}
Expand All @@ -1129,7 +1216,7 @@ public void PackageItemContainer_DoubleTapped(object sender, DoubleTappedRoutedE
{
if (sender is PackageItemContainer container && container.Package is not null)
{
CurrentPackageList.Select(container.Wrapper.Index);
SelectSingleIndex(container.Wrapper.Index);
container.Focus(FocusState.Keyboard);

TEL_InstallReferral referral = TEL_InstallReferral.ALREADY_INSTALLED;
Expand Down Expand Up @@ -1273,17 +1360,28 @@ public void PackageItemContainer_PreviewKeyDown(object sender, KeyRoutedEventArg
return;
}

int index = FilteredPackages.IndexOf(packageItemContainer.Wrapper);
bool IS_SHIFT_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
bool IS_CONTROL_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
bool IS_ALT_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftMenu).HasFlag(CoreVirtualKeyStates.Down);
IS_ALT_PRESSED |= InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightMenu).HasFlag(CoreVirtualKeyStates.Down);

// Use tracked navigation index when valid (e.g. after Shift+Arrow where
// focus intentionally stayed on the anchor item). Fall back to sender index.
int senderIndex = FilteredPackages.IndexOf(packageItemContainer.Wrapper);
int index = _lastNavigationIndex >= 0 && _lastNavigationIndex < FilteredPackages.Count
? _lastNavigationIndex
: senderIndex;

switch (e.Key)
{
case VirtualKey.Up when index > 0:
SelectAndScrollTo(index - 1, true); e.Handled = true; break;
SelectAndScrollTo(index - 1, true, IS_SHIFT_PRESSED); e.Handled = true; break;
case VirtualKey.Down when index < FilteredPackages.Count - 1:
SelectAndScrollTo(index + 1, true); e.Handled = true; break;
SelectAndScrollTo(index + 1, true, IS_SHIFT_PRESSED); e.Handled = true; break;
case VirtualKey.Home when index > 0:
SelectAndScrollTo(0, true); e.Handled = true; break;
SelectAndScrollTo(0, true, IS_SHIFT_PRESSED); e.Handled = true; break;
case VirtualKey.End when index < FilteredPackages.Count - 1:
SelectAndScrollTo(FilteredPackages.Count - 1, true); e.Handled = true; break;
SelectAndScrollTo(FilteredPackages.Count - 1, true, IS_SHIFT_PRESSED); e.Handled = true; break;
}

if (e.KeyStatus.WasKeyDown)
Expand All @@ -1294,11 +1392,6 @@ public void PackageItemContainer_PreviewKeyDown(object sender, KeyRoutedEventArg

IPackage? package = packageItemContainer.Package;

bool IS_CONTROL_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
//bool IS_SHIFT_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
bool IS_ALT_PRESSED = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftMenu).HasFlag(CoreVirtualKeyStates.Down);
IS_ALT_PRESSED |= InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightMenu).HasFlag(CoreVirtualKeyStates.Down);

if (e.Key == VirtualKey.Enter && package is not null)
{
if (IS_ALT_PRESSED)
Expand All @@ -1325,7 +1418,25 @@ public void PackageItemContainer_PreviewKeyDown(object sender, KeyRoutedEventArg
}
else if (e.Key == VirtualKey.Space && package is not null)
{
package.IsChecked = !package.IsChecked;
if (_selectedIndices.Count > 1)
{
// Toggle checkboxes for all selected items (contiguous or not)
int currentIndex = _lastNavigationIndex >= 0 && _lastNavigationIndex < FilteredPackages.Count
? _lastNavigationIndex
: senderIndex;
if (currentIndex < 0 || currentIndex >= FilteredPackages.Count)
return;
bool newState = !FilteredPackages[currentIndex].Package.IsChecked;
foreach (int i in _selectedIndices)
{
if (i >= 0 && i < FilteredPackages.Count)
FilteredPackages[i].IsChecked = newState;
}
}
else
{
packageItemContainer.Wrapper.IsChecked = !packageItemContainer.Wrapper.IsChecked;
}
e.Handled = true;
}
}
Expand Down Expand Up @@ -1406,6 +1517,7 @@ private void ViewModeSelector_SelectionChanged(object sender, SelectionChangedEv
{
Settings.SetDictionaryItem(Settings.K.PackageListViewMode, PAGE_NAME, ViewModeSelector.SelectedIndex);
GenerateHeaderBarTitles();
ResetSelectionState();
}

FrameworkElement _lastContextMenuButtonTapped = null!;
Expand Down
Loading