Skip to content

Commit 02246e4

Browse files
authored
Feature: Added support for reordering items in the sidebar (#11456)
1 parent c12e278 commit 02246e4

File tree

14 files changed

+290
-44
lines changed

14 files changed

+290
-44
lines changed

src/Files.App/DataModels/NavigationControlItems/DriveItem.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,6 @@ public static async Task<DriveItem> CreateFromPropertiesAsync(StorageFolder root
173173
ShowProperties = true
174174
};
175175
item.Path = string.IsNullOrEmpty(root.Path) ? $"\\\\?\\{root.Name}\\" : root.Path;
176-
App.Logger.Warn(item.Path);
177176
item.DeviceID = deviceId;
178177
item.Root = root;
179178

src/Files.App/DataModels/NavigationControlItems/LocationItem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ public bool IsExpanded
7272

7373
public ContextMenuOptions MenuOptions { get; set; }
7474

75+
public bool IsHeader { get; set; }
76+
7577
public int CompareTo(INavigationControlItem other)
7678
=> Text.CompareTo(other.Text);
7779

src/Files.App/DataModels/SidebarPinnedModel.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class SidebarPinnedModel
3030

3131
public List<string> FavoriteItems { get; set; } = new List<string>();
3232

33-
private readonly List<INavigationControlItem> favoriteList = new();
33+
public readonly List<INavigationControlItem> favoriteList = new();
3434

3535
[JsonIgnore]
3636
public IReadOnlyList<INavigationControlItem> Favorites
@@ -205,6 +205,8 @@ public async void LoadAsync(object? sender, FileSystemEventArgs e)
205205
}
206206

207207
public async Task LoadAsync()
208-
=> await UpdateItemsWithExplorer();
208+
{
209+
await UpdateItemsWithExplorer();
210+
}
209211
}
210212
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<ContentDialog
2+
x:Class="Files.App.Dialogs.ReorderSidebarItemsDialog"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:helpers="using:Files.App.Helpers"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
xmlns:navigationcontrolitems="using:Files.App.DataModels.NavigationControlItems"
9+
Title="{helpers:ResourceString Name=ReorderSidebarItemsDialogText}"
10+
DefaultButton="Primary"
11+
IsPrimaryButtonEnabled="True"
12+
PrimaryButtonCommand="{x:Bind ViewModel.PrimaryButtonCommand}"
13+
PrimaryButtonText="{helpers:ResourceString Name=Save}"
14+
RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}"
15+
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
16+
Style="{StaticResource DefaultContentDialogStyle}"
17+
mc:Ignorable="d">
18+
19+
<Grid
20+
x:Name="DestinationPathGrid"
21+
ColumnSpacing="8"
22+
RowSpacing="8">
23+
<Grid.ColumnDefinitions>
24+
<ColumnDefinition />
25+
<ColumnDefinition Width="Auto" />
26+
</Grid.ColumnDefinitions>
27+
<Grid.RowDefinitions>
28+
<RowDefinition Height="Auto" />
29+
</Grid.RowDefinitions>
30+
31+
<ListView
32+
x:Name="SidebarNavView"
33+
Grid.Row="1"
34+
ItemsSource="{x:Bind ViewModel.SidebarFavoriteItems, Mode=OneWay}">
35+
<ListView.ItemTemplate>
36+
<DataTemplate x:DataType="navigationcontrolitems:LocationItem">
37+
<Grid
38+
AllowDrop="True"
39+
AutomationProperties.AutomationId="{x:Bind Text}"
40+
DataContext="{x:Bind}"
41+
DragOver="ListViewItem_DragOver"
42+
DragStarting="ListViewItem_DragStarting"
43+
Drop="ListViewItem_Drop"
44+
Tag="{x:Bind Path}"
45+
ToolTipService.ToolTip="{x:Bind ToolTipText, Mode=OneWay}">
46+
<Grid.ColumnDefinitions>
47+
<ColumnDefinition Width="*" />
48+
<ColumnDefinition Width="Auto" />
49+
<ColumnDefinition Width="Auto" />
50+
</Grid.ColumnDefinitions>
51+
<Grid.RowDefinitions>
52+
<RowDefinition Height="Auto" />
53+
<RowDefinition Height="Auto" />
54+
</Grid.RowDefinitions>
55+
<TextBlock Grid.RowSpan="2" Text="{x:Bind Text}" />
56+
<FontIcon
57+
Grid.Row="1"
58+
HorizontalAlignment="Right"
59+
DataContext="{x:Bind}"
60+
Glyph="&#xE76F;"
61+
PointerPressed="MoveItem"
62+
Visibility="{x:Bind IsPinned}" />
63+
</Grid>
64+
</DataTemplate>
65+
</ListView.ItemTemplate>
66+
</ListView>
67+
</Grid>
68+
</ContentDialog>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using CommunityToolkit.WinUI.UI;
2+
using Files.App.DataModels.NavigationControlItems;
3+
using Files.App.Extensions;
4+
using Files.App.Filesystem;
5+
using Files.App.ServicesImplementation;
6+
using Files.App.ViewModels.Dialogs;
7+
using Files.Backend.ViewModels.Dialogs;
8+
using Files.Shared.Enums;
9+
using Microsoft.UI.Xaml;
10+
using Microsoft.UI.Xaml.Controls;
11+
using Microsoft.UI.Xaml.Input;
12+
using System;
13+
using System.Diagnostics;
14+
using System.Threading.Tasks;
15+
using Windows.ApplicationModel.DataTransfer;
16+
17+
namespace Files.App.Dialogs
18+
{
19+
public sealed partial class ReorderSidebarItemsDialog : ContentDialog, IDialog<ReorderSidebarItemsDialogViewModel>
20+
{
21+
public ReorderSidebarItemsDialogViewModel ViewModel
22+
{
23+
get => (ReorderSidebarItemsDialogViewModel)DataContext;
24+
set => DataContext = value;
25+
}
26+
27+
public ReorderSidebarItemsDialog()
28+
{
29+
InitializeComponent();
30+
}
31+
32+
private async void MoveItem(object sender, PointerRoutedEventArgs e)
33+
{
34+
var properties = e.GetCurrentPoint(null).Properties;
35+
var icon = sender as FontIcon;
36+
if (!properties.IsLeftButtonPressed)
37+
return;
38+
39+
var navItem = icon?.FindAscendant<Grid>();
40+
if (navItem is not null)
41+
await navItem.StartDragAsync(e.GetCurrentPoint(navItem));
42+
}
43+
44+
private void ListViewItem_DragStarting(object sender, DragStartingEventArgs e)
45+
{
46+
if (sender is not Grid nav || nav.DataContext is not LocationItem)
47+
return;
48+
49+
// Adding the original Location item dragged to the DragEvents data view
50+
e.Data.Properties.Add("sourceLocationItem", nav);
51+
e.AllowedOperations = DataPackageOperation.Move;
52+
}
53+
54+
55+
private void ListViewItem_DragOver(object sender, DragEventArgs e)
56+
{
57+
if ((sender as Grid)?.DataContext is not LocationItem locationItem)
58+
return;
59+
var deferral = e.GetDeferral();
60+
61+
if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem)
62+
{
63+
DragOver_SetCaptions(sourceLocationItem, locationItem, e);
64+
}
65+
66+
deferral.Complete();
67+
}
68+
69+
private void DragOver_SetCaptions(LocationItem senderLocationItem, LocationItem sourceLocationItem, DragEventArgs e)
70+
{
71+
// If the location item is the same as the original dragged item
72+
if (sourceLocationItem.CompareTo(senderLocationItem) == 0)
73+
{
74+
e.AcceptedOperation = DataPackageOperation.None;
75+
e.DragUIOverride.IsCaptionVisible = false;
76+
}
77+
else
78+
{
79+
e.DragUIOverride.IsCaptionVisible = true;
80+
e.DragUIOverride.Caption = "MoveItemsDialogPrimaryButtonText".GetLocalizedResource();
81+
e.AcceptedOperation = DataPackageOperation.Move;
82+
}
83+
}
84+
85+
private void ListViewItem_Drop(object sender, DragEventArgs e)
86+
{
87+
if (sender is not Grid navView || navView.DataContext is not LocationItem locationItem)
88+
return;
89+
90+
if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem)
91+
ViewModel.SidebarFavoriteItems.Move(ViewModel.SidebarFavoriteItems.IndexOf(sourceLocationItem), ViewModel.SidebarFavoriteItems.IndexOf(locationItem));
92+
}
93+
94+
public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();
95+
}
96+
}

src/Files.App/Files.App.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,4 @@
129129
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
130130
</Page>
131131
</ItemGroup>
132-
</Project>
132+
</Project>

src/Files.App/ServicesImplementation/DialogService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System;
1010
using System.Collections.Generic;
1111
using System.ComponentModel;
12+
using System.Diagnostics;
1213
using System.Threading.Tasks;
1314
using Windows.Foundation.Metadata;
1415

@@ -29,7 +30,8 @@ public DialogService()
2930
{ typeof(FileSystemDialogViewModel), () => new FilesystemOperationDialog() },
3031
{ typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() },
3132
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() },
32-
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() }
33+
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() },
34+
{ typeof(ReorderSidebarItemsDialogViewModel), () => new ReorderSidebarItemsDialog() }
3335
};
3436
}
3537

@@ -62,7 +64,9 @@ public Task<DialogResult> ShowDialogAsync<TViewModel>(TViewModel viewModel)
6264
}
6365
catch (Exception ex)
6466
{
65-
_ = ex;
67+
App.Logger.Warn(ex, "Failed to show dialog");
68+
69+
Debugger.Break();
6670
}
6771

6872
return Task.FromResult(DialogResult.None);

src/Files.App/ServicesImplementation/QuickAccessService.cs

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
using Files.App.Shell;
22
using Files.App.UserControls.Widgets;
3-
using Files.Sdk.Storage.LocatableStorage;
43
using Files.Shared;
54
using Files.Shared.Extensions;
6-
using Microsoft.UI.Xaml.Shapes;
75
using System;
86
using System.Collections.Generic;
9-
using System.IO;
107
using System.Linq;
118
using System.Threading.Tasks;
129

@@ -23,32 +20,47 @@ public async Task<IEnumerable<ShellFileItem>> GetPinnedFoldersAsync()
2320
}
2421

2522
public Task PinToSidebar(string folderPath)
26-
=> PinToSidebar(new[] { folderPath });
23+
{
24+
return PinToSidebar(new[] { folderPath });
25+
}
2726

2827
public async Task PinToSidebar(string[] folderPaths)
2928
{
30-
await ContextMenu.InvokeVerb("pintohome", folderPaths);
29+
foreach (string folderPath in folderPaths)
30+
await ContextMenu.InvokeVerb("pintohome", new[] {folderPath});
3131

3232
await App.QuickAccessManager.Model.LoadAsync();
3333

3434
App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(folderPaths, true));
3535
}
36-
36+
3737
public Task UnpinFromSidebar(string folderPath)
38-
=> UnpinFromSidebar(new[] { folderPath });
38+
{
39+
return UnpinFromSidebar(new[] { folderPath });
40+
}
3941

4042
public async Task UnpinFromSidebar(string[] folderPaths)
4143
{
4244
Type? shellAppType = Type.GetTypeFromProgID("Shell.Application");
4345
object? shell = Activator.CreateInstance(shellAppType);
4446
dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, new object[] { $"shell:{guid}" });
4547

48+
if (folderPaths.Length == 0)
49+
folderPaths = (await GetPinnedFoldersAsync())
50+
.Where(link => (bool?)link.Properties["System.Home.IsPinned"] ?? false)
51+
.Select(link => link.FilePath).ToArray();
52+
4653
foreach (dynamic? fi in f2.Items())
54+
{
4755
if (folderPaths.Contains((string)fi.Path)
4856
|| (string.Equals(fi.Path, "::{645FF040-5081-101B-9F08-00AA002F954E}") && folderPaths.Contains(Constants.CommonPaths.RecycleBinPath)))
49-
await SafetyExtensions.IgnoreExceptions(async () => {
57+
{
58+
await SafetyExtensions.IgnoreExceptions(async () =>
59+
{
5060
await fi.InvokeVerb("unpinfromhome");
5161
});
62+
}
63+
}
5264

5365
await App.QuickAccessManager.Model.LoadAsync();
5466

@@ -59,5 +71,20 @@ public bool IsItemPinned(string folderPath)
5971
{
6072
return App.QuickAccessManager.Model.FavoriteItems.Contains(folderPath);
6173
}
74+
75+
public async Task Save(string[] items)
76+
{
77+
if (Equals(items, App.QuickAccessManager.Model.FavoriteItems.ToArray()))
78+
return;
79+
80+
App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = false;
81+
82+
// Unpin every item that is below this index and then pin them all in order
83+
await UnpinFromSidebar(Array.Empty<string>());
84+
85+
await PinToSidebar(items);
86+
App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true;
87+
await App.QuickAccessManager.Model.LoadAsync();
88+
}
6289
}
6390
}

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2595,6 +2595,9 @@
25952595
<data name="MultiSelect" xml:space="preserve">
25962596
<value>Multiselect</value>
25972597
</data>
2598+
<data name="ReorderSidebarItemsDialogText" xml:space="preserve">
2599+
<value>Reorder sidebar items</value>
2600+
</data>
25982601
<data name="Hashes" xml:space="preserve">
25992602
<value>Hashes</value>
26002603
</data>

src/Files.App/UserControls/SidebarControl.xaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
DragEnter="NavigationViewItem_DragEnter"
6363
DragLeave="NavigationViewItem_DragLeave"
6464
DragOver="NavigationViewLocationItem_DragOver"
65-
DragStarting="NavigationViewItem_DragStarting"
6665
Drop="NavigationViewLocationItem_Drop"
6766
IsExpanded="{x:Bind IsExpanded, Mode=TwoWay}"
6867
IsRightTapEnabled="True"

0 commit comments

Comments
 (0)