diff --git a/src/Files.App.Controls/Sidebar/DragDropExceptionHelper.cs b/src/Files.App.Controls/Sidebar/DragDropExceptionHelper.cs
new file mode 100644
index 000000000000..df9cf72e941e
--- /dev/null
+++ b/src/Files.App.Controls/Sidebar/DragDropExceptionHelper.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Files Community
+// Licensed under the MIT License.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Files.App.Controls
+{
+ ///
+ /// Provides helper methods for classifying expected drag-and-drop COM failures
+ /// caused by stale OLE drag payloads (e.g. from Windows Explorer).
+ ///
+ internal static class DragDropExceptionHelper
+ {
+ // CLIPBRD_E_CANT_OPEN / OLE_E_NOTRUNNING: clipboard/data object is no longer available
+ private const int HRESULT_CLIPBOARD_DATA_UNAVAILABLE = unchecked((int)0x800401D0);
+
+ // RPC_E_SERVERFAULT: OLE/RPC drag pipeline failure (stale cross-process drag)
+ private const int HRESULT_RPC_OLE_FAILURE = unchecked((int)0x80010105);
+
+ ///
+ /// Returns when is a
+ /// with an HResult that indicates a stale or already-released OLE drag payload.
+ /// These are expected during sidebar reorder when the user also has File Explorer open.
+ ///
+ public static bool IsExpectedStaleDragData(Exception ex)
+ {
+ return ex is COMException com &&
+ (com.HResult == HRESULT_CLIPBOARD_DATA_UNAVAILABLE ||
+ com.HResult == HRESULT_RPC_OLE_FAILURE);
+ }
+
+ ///
+ /// Writes a debug-level trace for a stale drag payload event.
+ ///
+ [Conditional("DEBUG")]
+ public static void LogStaleDrag(Exception ex, string message)
+ {
+ Debug.WriteLine($"[DragDrop] {message} HResult=0x{ex.HResult:X8}");
+ }
+ }
+}
diff --git a/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs b/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs
index ab0e4742ae6c..63b304561d72 100644
--- a/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs
+++ b/src/Files.App.Controls/Sidebar/ISidebarItemModel.cs
@@ -21,4 +21,17 @@ public interface ISidebarItemModel : INotifyPropertyChanged
///
bool PaddedItem { get; }
}
+
+ public interface IDraggableSidebarItemModel : ISidebarItemModel
+ {
+ ///
+ /// The file path used for drag and drop operations
+ ///
+ string? DropPath { get; }
+
+ ///
+ /// Indicates whether the item supports reorder dropping
+ ///
+ bool IsReorderDropItem { get; }
+ }
}
diff --git a/src/Files.App.Controls/Sidebar/SidebarItem.cs b/src/Files.App.Controls/Sidebar/SidebarItem.cs
index ddd0a3df3dc5..a1ae180d843c 100644
--- a/src/Files.App.Controls/Sidebar/SidebarItem.cs
+++ b/src/Files.App.Controls/Sidebar/SidebarItem.cs
@@ -92,7 +92,17 @@ public void HandleItemChange()
HookupItemChangeListener(null, Item);
UpdateExpansionState();
ReevaluateSelection();
- CanDrag = Item?.GetType().GetProperty("Path")?.GetValue(Item) is string path && Path.IsPathRooted(path);
+
+ if (Item is IDraggableSidebarItemModel draggableItem)
+ {
+ CanDrag = IsValidDropPath(draggableItem.DropPath);
+ UseReorderDrop = !IsGroupHeader && CanDrag && draggableItem.IsReorderDropItem;
+ }
+ else
+ {
+ CanDrag = false;
+ UseReorderDrop = false;
+ }
}
private void HookupOwners()
@@ -138,32 +148,51 @@ private void HookupItemChangeListener(ISidebarItemModel? oldItem, ISidebarItemMo
}
}
+ private static bool IsValidDropPath(string? path)
+ => path is not null && (System.IO.Path.IsPathRooted(path) || path.StartsWith("Shell:", StringComparison.OrdinalIgnoreCase));
+
private void SidebarItem_DragStarting(UIElement sender, DragStartingEventArgs args)
{
- if (Item?.GetType().GetProperty("Path")?.GetValue(Item) is not string dragPath || !Path.IsPathRooted(dragPath))
+ if (Item is not IDraggableSidebarItemModel draggableItem || draggableItem.DropPath is not string dragPath || !IsValidDropPath(dragPath))
return;
- args.Data.SetData(StandardDataFormats.Text, dragPath);
- args.Data.RequestedOperation = DataPackageOperation.Move | DataPackageOperation.Copy | DataPackageOperation.Link;
- args.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
+ try
{
- var deferral = request.GetDeferral();
- try
+ args.Data.SetData(StandardDataFormats.Text, dragPath);
+ args.Data.RequestedOperation = DataPackageOperation.Move | DataPackageOperation.Copy | DataPackageOperation.Link;
+ args.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
{
- if (Directory.Exists(dragPath))
+ var deferral = request.GetDeferral();
+ try
{
- var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
- request.SetData(new IStorageItem[] { folder });
+ if (Directory.Exists(dragPath))
+ {
+ var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
+ request.SetData(new IStorageItem[] { folder });
+ }
}
- }
- catch
- {
- }
- finally
- {
- deferral.Complete();
- }
- });
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload while resolving StorageFolder in data provider.");
+ }
+ finally
+ {
+ try
+ {
+ deferral.Complete();
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral during drag data provider completion.");
+ }
+ }
+ });
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload on DragStarting, cancelling drag.");
+ args.Cancel = true;
+ }
}
private void SetFlyoutOpen(bool isOpen = true)
@@ -394,21 +423,61 @@ private async void ItemBorder_DragOver(object sender, DragEventArgs e)
IsExpanded = true;
}
- var insertsAbove = DetermineDropTargetPosition(e);
- if (insertsAbove == SidebarItemDropPosition.Center)
+ DragOperationDeferral? deferral = null;
+ try
{
- VisualStateManager.GoToState(this, "DragOnTop", true);
+ deferral = e.GetDeferral();
}
- else if (insertsAbove == SidebarItemDropPosition.Top)
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
{
- VisualStateManager.GoToState(this, "DragInsertAbove", true);
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload on GetDeferral during DragOver.");
+ VisualStateManager.GoToState(this, "Normal", true);
+ return;
}
- else if (insertsAbove == SidebarItemDropPosition.Bottom)
+
+ try
{
- VisualStateManager.GoToState(this, "DragInsertBelow", true);
- }
+ var dropPosition = DetermineDropTargetPosition(e);
- Owner?.RaiseItemDragOver(this, insertsAbove, e);
+ if (Owner is not null)
+ Owner.RaiseItemDragOver(this, dropPosition, e);
+
+ if (!e.Handled || e.AcceptedOperation == DataPackageOperation.None)
+ {
+ VisualStateManager.GoToState(this, "Normal", true);
+ return;
+ }
+
+ if (dropPosition == SidebarItemDropPosition.Center)
+ {
+ VisualStateManager.GoToState(this, "DragOnTop", true);
+ }
+ else if (dropPosition == SidebarItemDropPosition.Top)
+ {
+ VisualStateManager.GoToState(this, "DragInsertAbove", true);
+ }
+ else if (dropPosition == SidebarItemDropPosition.Bottom)
+ {
+ VisualStateManager.GoToState(this, "DragInsertBelow", true);
+ }
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar DragOver processing.");
+ e.AcceptedOperation = DataPackageOperation.None;
+ VisualStateManager.GoToState(this, "Normal", true);
+ }
+ finally
+ {
+ try
+ {
+ deferral?.Complete();
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral on DragOver completion.");
+ }
+ }
}
private void ItemBorder_ContextRequested(UIElement sender, Microsoft.UI.Xaml.Input.ContextRequestedEventArgs args)
@@ -425,7 +494,16 @@ private void ItemBorder_DragLeave(object sender, DragEventArgs e)
private void ItemBorder_Drop(object sender, DragEventArgs e)
{
UpdatePointerState();
- Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
+ try
+ {
+ Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar Drop, drop discarded.");
+ e.AcceptedOperation = DataPackageOperation.None;
+ e.Handled = true;
+ }
}
private SidebarItemDropPosition DetermineDropTargetPosition(DragEventArgs args)
diff --git a/src/Files.App.Controls/Sidebar/SidebarView.xaml.cs b/src/Files.App.Controls/Sidebar/SidebarView.xaml.cs
index 1ec03c66f9f4..66c99291da97 100644
--- a/src/Files.App.Controls/Sidebar/SidebarView.xaml.cs
+++ b/src/Files.App.Controls/Sidebar/SidebarView.xaml.cs
@@ -4,6 +4,7 @@
using Microsoft.UI.Input;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Markup;
+using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Core;
@@ -53,13 +54,31 @@ internal void RaiseContextRequested(SidebarItem item, Point e)
internal void RaiseItemDropped(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
{
if (sideBarItem.Item is null) return;
- ItemDropped?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
+
+ try
+ {
+ ItemDropped?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDropped.");
+ return;
+ }
}
internal void RaiseItemDragOver(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
{
if (sideBarItem.Item is null) return;
- ItemDragOver?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
+
+ try
+ {
+ var args = new ItemDragOverEventArgs(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent);
+ ItemDragOver?.Invoke(this, args);
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDragOver.");
+ }
}
private void UpdateMinimalMode()
diff --git a/src/Files.App/Data/Contracts/INavigationControlItem.cs b/src/Files.App/Data/Contracts/INavigationControlItem.cs
index da7f64839e41..ce535fe23f83 100644
--- a/src/Files.App/Data/Contracts/INavigationControlItem.cs
+++ b/src/Files.App/Data/Contracts/INavigationControlItem.cs
@@ -5,12 +5,16 @@
namespace Files.App.Data.Contracts
{
- public interface INavigationControlItem : IComparable, INotifyPropertyChanged, ISidebarItemModel
+ public interface INavigationControlItem : IComparable, INotifyPropertyChanged, IDraggableSidebarItemModel
{
public new string Text { get; }
public string Path { get; }
+ string? IDraggableSidebarItemModel.DropPath => Path;
+
+ bool IDraggableSidebarItemModel.IsReorderDropItem => Section == SectionType.Pinned;
+
public SectionType Section { get; }
public NavigationControlItemType ItemType { get; }
diff --git a/src/Files.App/Data/Items/LocationItem.cs b/src/Files.App/Data/Items/LocationItem.cs
index 0d5b2dc5bed2..36a894f95660 100644
--- a/src/Files.App/Data/Items/LocationItem.cs
+++ b/src/Files.App/Data/Items/LocationItem.cs
@@ -48,6 +48,8 @@ public string Path
ToolTip = string.IsNullOrEmpty(Path) ||
Path.Contains('?', StringComparison.Ordinal) ||
Path.StartsWith("shell:", StringComparison.OrdinalIgnoreCase) ||
+ Path.StartsWith("::{", StringComparison.Ordinal) ||
+ Path.StartsWith(@"\\SHELL\", StringComparison.OrdinalIgnoreCase) ||
Path.EndsWith(ShellLibraryItem.EXTENSION, StringComparison.OrdinalIgnoreCase) ||
Path == "Home" ||
Path == "ReleaseNotes" ||
diff --git a/src/Files.App/Data/Models/PinnedFoldersManager.cs b/src/Files.App/Data/Models/PinnedFoldersManager.cs
index 33eef2120c63..bc15e49857bf 100644
--- a/src/Files.App/Data/Models/PinnedFoldersManager.cs
+++ b/src/Files.App/Data/Models/PinnedFoldersManager.cs
@@ -1,6 +1,7 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
+using Microsoft.Extensions.Logging;
using System.Collections.Specialized;
using System.IO;
@@ -17,6 +18,33 @@ public sealed class PinnedFoldersManager
public List PinnedFolders { get; set; } = [];
+ private int _syncSuspendCount;
+
+ ///
+ /// Returns true when sync is suspended
+ ///
+ public bool IsSyncSuspended => _syncSuspendCount > 0;
+
+ ///
+ /// Suspends sync operations until the returned value is disposed
+ ///
+ public IDisposable SuspendSync()
+ {
+ Interlocked.Increment(ref _syncSuspendCount);
+ return new SyncSuspensionScope(this);
+ }
+
+ private sealed class SyncSuspensionScope(PinnedFoldersManager owner) : IDisposable
+ {
+ private int _disposed;
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 0)
+ Interlocked.Decrement(ref owner._syncSuspendCount);
+ }
+ }
+
public readonly List _PinnedFolderItems = [];
[JsonIgnore]
@@ -34,6 +62,9 @@ public IReadOnlyList PinnedFolderItems
///
public async Task UpdateItemsWithExplorerAsync()
{
+ if (IsSyncSuspended)
+ return;
+
await addSyncSemaphore.WaitAsync();
try
@@ -46,9 +77,26 @@ public async Task UpdateItemsWithExplorerAsync()
if (formerPinnedFolders.SequenceEqual(PinnedFolders))
return;
+ if (formerPinnedFolders.Count == PinnedFolders.Count &&
+ new HashSet(formerPinnedFolders, StringComparer.OrdinalIgnoreCase)
+ .SetEquals(PinnedFolders))
+ {
+ ApplyReorderToPinnedItems();
+ return;
+ }
RemoveStaleSidebarItems();
- await AddAllItemsToSidebarAsync();
+ foreach (var path in PinnedFolders)
+ {
+ bool exists;
+ lock (_PinnedFolderItems)
+ {
+ exists = _PinnedFolderItems.Any(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+ }
+ if (!exists)
+ await AddItemToSidebarAsync(path);
+ }
+ ApplyReorderToPinnedItems();
}
finally
{
@@ -56,6 +104,95 @@ public async Task UpdateItemsWithExplorerAsync()
}
}
+ ///
+ /// Reorders and to match
+ /// without firing events.
+ /// Only intended to be called from SidebarViewModel.
+ ///
+ internal void UpdateOrderSilently(string[] newOrder)
+ {
+ lock (_PinnedFolderItems)
+ {
+ ReorderPinnedItemsCore(newOrder, moves: null);
+ }
+
+ PinnedFolders = newOrder.ToList();
+ }
+
+ private void ApplyReorderToPinnedItems()
+ {
+ var moves = new List<(INavigationControlItem item, int newIndex, int oldIndex)>();
+
+ lock (_PinnedFolderItems)
+ {
+ ReorderPinnedItemsCore(PinnedFolders, moves);
+ }
+
+ foreach (var move in moves)
+ {
+ DataChanged?.Invoke(SectionType.Pinned, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, move.item, move.newIndex, move.oldIndex));
+ }
+ }
+
+ ///
+ /// Reorders to match .
+ /// Must be called while holding the _PinnedFolderItems lock
+ ///
+ private void ReorderPinnedItemsCore(IList desiredOrder, List<(INavigationControlItem item, int newIndex, int oldIndex)>? moves)
+ {
+ int baseIndex = GetPinnedItemsBaseIndex();
+
+ for (int i = 0; i < desiredOrder.Count; i++)
+ {
+ var path = desiredOrder[i];
+ var currentItem = _PinnedFolderItems.FirstOrDefault(x => x.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
+ if (currentItem is null)
+ continue;
+
+ int oldIndex = _PinnedFolderItems.IndexOf(currentItem);
+ int newIndex = baseIndex + i;
+
+ if (oldIndex != newIndex && newIndex < _PinnedFolderItems.Count)
+ {
+ _PinnedFolderItems.RemoveAt(oldIndex);
+ _PinnedFolderItems.Insert(newIndex, currentItem);
+ moves?.Add((currentItem, newIndex, oldIndex));
+ }
+ }
+ }
+
+ ///
+ /// Returns the base index of user-pinned items in .
+ /// Must be called while holding the _PinnedFolderItems lock.
+ ///
+ /// Invariant assumed: default-location items always appear before user-pinned items and
+ /// are never interspersed with them. If the first non-default item is found, that is the
+ /// base index.
+ ///
+ ///
+ private int GetPinnedItemsBaseIndex()
+ {
+ int firstPinnedIndex = _PinnedFolderItems.FindIndex(x => x is LocationItem item && !item.IsDefaultLocation);
+ if (firstPinnedIndex >= 0)
+ {
+ // Default locations should always appear before user-pinned locations
+ var hasDefaultAfterFirstPinned = _PinnedFolderItems
+ .Skip(firstPinnedIndex)
+ .Any(x => x is LocationItem item && item.IsDefaultLocation);
+
+ Debug.Assert(!hasDefaultAfterFirstPinned);
+
+ if (!hasDefaultAfterFirstPinned)
+ return firstPinnedIndex;
+
+ int lastDefaultIndex = _PinnedFolderItems.FindLastIndex(x => x is LocationItem item && item.IsDefaultLocation);
+ return lastDefaultIndex == -1 ? 0 : lastDefaultIndex + 1;
+ }
+
+ int trailingDefaultIndex = _PinnedFolderItems.FindLastIndex(x => x is LocationItem item && item.IsDefaultLocation);
+ return trailingDefaultIndex == -1 ? 0 : trailingDefaultIndex + 1;
+ }
+
///
/// Returns the index of the location item in the navigation sidebar
///
@@ -198,8 +335,7 @@ public async Task AddAllItemsToSidebarAsync()
///
public void RemoveStaleSidebarItems()
{
- // Remove unpinned items from PinnedFolderItems
- foreach (var childItem in PinnedFolderItems)
+ foreach (var childItem in PinnedFolderItems.ToList())
{
if (childItem is LocationItem item && !item.IsDefaultLocation && !PinnedFolders.Contains(item.Path))
{
@@ -210,18 +346,23 @@ public void RemoveStaleSidebarItems()
DataChanged?.Invoke(SectionType.Pinned, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item));
}
}
-
- // Remove unpinned items from sidebar
- DataChanged?.Invoke(SectionType.Pinned, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public async void LoadAsync(object? sender, FileSystemEventArgs e)
{
- await LoadAsync();
- App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(null, new ModifyQuickAccessEventArgs((await QuickAccessService.GetPinnedFoldersAsync()).ToArray(), true)
+ try
+ {
+ await LoadAsync();
+ var pinnedFolders = await QuickAccessService.GetPinnedFoldersAsync();
+ App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(null, new ModifyQuickAccessEventArgs(pinnedFolders.ToArray(), true)
+ {
+ Reset = true
+ });
+ }
+ catch (Exception ex)
{
- Reset = true
- });
+ App.Logger.LogWarning(ex, "Error loading pinned folders from watcher");
+ }
}
public async Task LoadAsync()
diff --git a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml
deleted file mode 100644
index 5550d40c5ae1..000000000000
--- a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs b/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs
deleted file mode 100644
index b5be41005632..000000000000
--- a/src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright (c) Files Community
-// Licensed under the MIT License.
-
-using CommunityToolkit.WinUI;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Input;
-using Windows.ApplicationModel.DataTransfer;
-
-namespace Files.App.Dialogs
-{
- public sealed partial class ReorderSidebarItemsDialog : ContentDialog, IDialog
- {
- private FrameworkElement RootAppElement
- => (FrameworkElement)MainWindow.Instance.Content;
-
- public ReorderSidebarItemsDialogViewModel ViewModel
- {
- get => (ReorderSidebarItemsDialogViewModel)DataContext;
- set => DataContext = value;
- }
-
- public ReorderSidebarItemsDialog()
- {
- InitializeComponent();
- }
-
- private async void MoveItemAsync(object sender, PointerRoutedEventArgs e)
- {
- var properties = e.GetCurrentPoint(null).Properties;
- if (!properties.IsLeftButtonPressed)
- return;
-
- var icon = sender as FontIcon;
-
- var navItem = icon?.FindAscendant();
- if (navItem is not null)
- await navItem.StartDragAsync(e.GetCurrentPoint(navItem));
- }
-
- private void ListViewItem_DragStarting(object sender, DragStartingEventArgs e)
- {
- if (sender is not Grid nav || nav.DataContext is not LocationItem)
- return;
-
- // Adding the original Location item dragged to the DragEvents data view
- e.Data.Properties.Add("sourceLocationItem", nav);
- e.AllowedOperations = DataPackageOperation.Move;
- }
-
- private void ListViewItem_DragOver(object sender, DragEventArgs e)
- {
- if ((sender as Grid)?.DataContext is not LocationItem locationItem)
- return;
- var deferral = e.GetDeferral();
-
- if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem)
- {
- DragOver_SetCaptions(sourceLocationItem, locationItem, e);
- }
-
- deferral.Complete();
- }
-
- private void DragOver_SetCaptions(LocationItem senderLocationItem, LocationItem sourceLocationItem, DragEventArgs e)
- {
- // If the location item is the same as the original dragged item
- if (sourceLocationItem.CompareTo(senderLocationItem) == 0)
- {
- e.AcceptedOperation = DataPackageOperation.None;
- e.DragUIOverride.IsCaptionVisible = false;
- }
- else
- {
- e.DragUIOverride.IsCaptionVisible = true;
- e.DragUIOverride.Caption = Strings.MoveItemsDialogPrimaryButtonText.GetLocalizedResource();
- e.AcceptedOperation = DataPackageOperation.Move;
- }
- }
-
- private void ListViewItem_Drop(object sender, DragEventArgs e)
- {
- if (sender is not Grid navView || navView.DataContext is not LocationItem locationItem)
- return;
-
- if ((e.DataView.Properties["sourceLocationItem"] as Grid)?.DataContext is LocationItem sourceLocationItem)
- ViewModel.SidebarPinnedFolderItems.Move(ViewModel.SidebarPinnedFolderItems.IndexOf(sourceLocationItem), ViewModel.SidebarPinnedFolderItems.IndexOf(locationItem));
- }
-
- public new async Task ShowAsync()
- {
- return (DialogResult)await base.ShowAsync();
- }
- }
-}
diff --git a/src/Files.App/Services/App/AppDialogService.cs b/src/Files.App/Services/App/AppDialogService.cs
index 418eaf3d6d55..2ccf12c28410 100644
--- a/src/Files.App/Services/App/AppDialogService.cs
+++ b/src/Files.App/Services/App/AppDialogService.cs
@@ -25,7 +25,6 @@ public DialogService()
{ typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() },
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() },
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() },
- { typeof(ReorderSidebarItemsDialogViewModel), () => new ReorderSidebarItemsDialog() },
{ typeof(AddBranchDialogViewModel), () => new AddBranchDialog() },
{ typeof(GitHubLoginDialogViewModel), () => new GitHubLoginDialog() },
{ typeof(FileTooLargeDialogViewModel), () => new FileTooLargeDialog() },
diff --git a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs
index 86cc2008cefd..89a28a76424d 100644
--- a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs
+++ b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs
@@ -1,6 +1,10 @@
-// Copyright (c) Files Community
+// Copyright (c) Files Community
// Licensed under the MIT License.
+using System.IO;
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+
namespace Files.App.Services
{
internal sealed class QuickAccessService : IQuickAccessService
@@ -8,6 +12,8 @@ internal sealed class QuickAccessService : IQuickAccessService
// Quick access shell folder (::{679f85cb-0220-4080-b29b-5540cc05aab6}) contains recent files
// which are unnecessary for getting pinned folders, so we use frequent places shell folder instead.
private readonly static string guid = "::{3936e9e4-d92c-4eee-a85a-bc16d5ea0819}";
+ private static readonly TimeSpan UnpinSettleTimeout = TimeSpan.FromSeconds(5);
+ private static readonly TimeSpan ReconciliationTimeout = TimeSpan.FromSeconds(5);
public async Task> GetPinnedFoldersAsync()
{
@@ -20,14 +26,51 @@ public async Task> GetPinnedFoldersAsync()
public Task PinToSidebarAsync(string[] folderPaths) => PinToSidebarAsync(folderPaths, true);
- private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget)
+ private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget, bool force = false)
{
foreach (string folderPath in folderPaths)
{
// make sure that the item has not yet been pinned
// the verb 'pintohome' is for both adding and removing
- if (!IsItemPinned(folderPath))
- await ContextMenu.InvokeVerb("pintohome", folderPath);
+ if (force || !IsItemPinned(folderPath))
+ {
+ if (ShellStorageFolder.IsShellPath(folderPath))
+ {
+ bool success = false;
+ await STATask.Run(() =>
+ {
+ Type? shellAppType = Type.GetTypeFromProgID("Shell.Application");
+ if (shellAppType == null)
+ return;
+
+ object? shell = Activator.CreateInstance(shellAppType);
+ string pathForShell = folderPath;
+ if (folderPath.StartsWith(@"\\SHELL\", StringComparison.OrdinalIgnoreCase))
+ {
+ using var shellItem = ShellFolderExtensions.GetShellItemFromPathOrPIDL(folderPath);
+ if (shellItem is null)
+ return;
+ pathForShell = shellItem.ParsingName ?? folderPath;
+ }
+
+ object? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [pathForShell]);
+ if (f2 != null)
+ {
+ object? fi = f2.GetType().InvokeMember("Self", System.Reflection.BindingFlags.GetProperty, null, f2, []);
+ success = TryInvokeShellVerb(fi, "pintohome", pathForShell);
+ }
+ }, App.Logger);
+
+ if (!success)
+ {
+ await ContextMenu.InvokeVerb("pintohome", folderPath);
+ }
+ }
+ else
+ {
+ await ContextMenu.InvokeVerb("pintohome", folderPath);
+ }
+ }
}
await App.QuickAccessManager.Model.LoadAsync();
@@ -41,74 +84,327 @@ private async Task PinToSidebarAsync(string[] folderPaths, bool doUpdateQuickAcc
private async Task UnpinFromSidebarAsync(string[] folderPaths, bool doUpdateQuickAccessWidget)
{
- Type? shellAppType = Type.GetTypeFromProgID("Shell.Application");
- object? shell = Activator.CreateInstance(shellAppType);
- dynamic? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [$"shell:{guid}"]);
+ folderPaths = NormalizeAndDeduplicatePaths(folderPaths);
if (folderPaths.Length == 0)
- folderPaths = (await GetPinnedFoldersAsync())
+ {
+ folderPaths = NormalizeAndDeduplicatePaths((await GetPinnedFoldersAsync())
.Where(link => (bool?)link.Properties["System.Home.IsPinned"] ?? false)
- .Select(link => link.FilePath).ToArray();
+ .Select(link => link.FilePath)
+ .ToArray());
+ }
- foreach (dynamic? fi in f2.Items())
+ try
{
- string pathStr = (string)fi.Path;
+ Type? shellAppType = Type.GetTypeFromProgID("Shell.Application");
+ if (shellAppType == null)
+ return;
+
+ object? shell = Activator.CreateInstance(shellAppType);
+ object? f2 = shellAppType.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shell, [$"shell:{guid}"]);
+ if (f2 == null)
+ return;
- if (ShellStorageFolder.IsShellPath(pathStr))
+ List pathsToUnpin = new();
+ var normalizedTargetPaths = BuildNormalizedPathSet(folderPaths);
+
+ object? items = f2.GetType().InvokeMember("Items", System.Reflection.BindingFlags.InvokeMethod, null, f2, []);
+ if (items is System.Collections.IEnumerable enumerable)
{
- var folder = await ShellStorageFolder.FromPathAsync(pathStr);
- var path = folder?.Path;
+ foreach (object? fi in enumerable)
+ {
+ if (fi is null) continue;
+ string pathStr = (string)fi.GetType().InvokeMember("Path", System.Reflection.BindingFlags.GetProperty, null, fi, [])!;
+ var normalizedPathStr = NormalizeQuickAccessPath(pathStr);
+ bool shouldUnpin = normalizedTargetPaths.Contains(normalizedPathStr);
- if (path is not null &&
- (folderPaths.Contains(path) ||
- (path.StartsWith(@"\\SHELL\\") && folderPaths.Any(x => x.StartsWith(@"\\SHELL\\")))))
+ if (!shouldUnpin && ShellStorageFolder.IsShellPath(pathStr))
{
- await STATask.Run(async () =>
- {
- fi.InvokeVerb("unpinfromhome");
- }, App.Logger);
- continue;
+ var folder = await ShellStorageFolder.FromPathAsync(pathStr);
+ var path = folder?.Path;
+
+ if (!string.IsNullOrWhiteSpace(path))
+ shouldUnpin = normalizedTargetPaths.Contains(NormalizeQuickAccessPath(path));
+ }
+
+ if (shouldUnpin)
+ {
+ pathsToUnpin.Add(pathStr);
}
}
+ }
- if (folderPaths.Contains(pathStr))
+ if (pathsToUnpin.Count > 0)
{
- await STATask.Run(async () =>
+ var normalizedPathsToUnpin = BuildNormalizedPathSet(pathsToUnpin);
+ await STATask.Run(() =>
{
- fi.InvokeVerb("unpinfromhome");
+ Type? shellAppTypeSTA = Type.GetTypeFromProgID("Shell.Application");
+ if (shellAppTypeSTA == null) return;
+ object? shellSTA = Activator.CreateInstance(shellAppTypeSTA);
+ object? f2STA = shellAppTypeSTA.InvokeMember("NameSpace", System.Reflection.BindingFlags.InvokeMethod, null, shellSTA, [$"shell:{guid}"]);
+ if (f2STA == null) return;
+
+ object? itemsSTA = f2STA.GetType().InvokeMember("Items", System.Reflection.BindingFlags.InvokeMethod, null, f2STA, []);
+ if (itemsSTA is System.Collections.IEnumerable enumerableSTA)
+ {
+ foreach (object? fi in enumerableSTA)
+ {
+ if (fi is null) continue;
+ string pathStr = (string)fi.GetType().InvokeMember("Path", System.Reflection.BindingFlags.GetProperty, null, fi, [])!;
+ if (normalizedPathsToUnpin.Contains(NormalizeQuickAccessPath(pathStr)))
+ {
+ var unpinned = TryInvokeShellVerb(fi, "unpinfromhome", pathStr);
+ if (!unpinned && ShellStorageFolder.IsShellPath(pathStr))
+ TryInvokeShellVerb(fi, "remove", pathStr);
+ }
+ }
+ }
}, App.Logger);
}
}
-
- await App.QuickAccessManager.Model.LoadAsync();
- if (doUpdateQuickAccessWidget)
- App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(folderPaths, false));
+ finally
+ {
+ await App.QuickAccessManager.Model.LoadAsync();
+ if (doUpdateQuickAccessWidget)
+ App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(folderPaths, false));
+ }
}
public bool IsItemPinned(string folderPath)
{
- return App.QuickAccessManager.Model.PinnedFolders.Contains(folderPath);
+ if (App.QuickAccessManager.Model.PinnedFolders.Contains(folderPath, StringComparer.OrdinalIgnoreCase))
+ return true;
+
+ if (!ShellStorageFolder.IsShellPath(folderPath))
+ return false;
+
+ var normalizedPath = NormalizeQuickAccessPath(folderPath);
+ return App.QuickAccessManager.Model.PinnedFolders
+ .Any(x => string.Equals(NormalizeQuickAccessPath(x), normalizedPath, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static bool TryInvokeShellVerb(object? shellItem, string verb, string path)
+ {
+ if (shellItem is null)
+ return false;
+
+ try
+ {
+ shellItem.GetType().InvokeMember("InvokeVerb", System.Reflection.BindingFlags.InvokeMethod, null, shellItem, [verb]);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogDebug(ex, "Failed to invoke shell verb {Verb} for {Path}", verb, path);
+ return false;
+ }
+ }
+
+ private static string[] NormalizeAndDeduplicatePaths(IEnumerable? paths)
+ {
+ if (paths is null)
+ return [];
+
+ List result = [];
+ HashSet normalizedSet = new(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var path in paths.Where(x => !string.IsNullOrWhiteSpace(x)))
+ {
+ var normalizedPath = NormalizeQuickAccessPath(path);
+ if (normalizedSet.Add(normalizedPath))
+ result.Add(path);
+ }
+
+ return result.ToArray();
+ }
+
+ private static HashSet BuildNormalizedPathSet(IEnumerable paths)
+ {
+ return new HashSet(
+ paths
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(NormalizeQuickAccessPath),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ private static string NormalizeQuickAccessPath(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return string.Empty;
+
+ if (!ShellStorageFolder.IsShellPath(path))
+ return path;
+
+ try
+ {
+ using var shellItem = ShellFolderExtensions.GetShellItemFromPathOrPIDL(path);
+ var parsingName = shellItem?.ParsingName;
+ if (!string.IsNullOrWhiteSpace(parsingName))
+ return parsingName;
+ }
+ catch (COMException ex)
+ {
+ App.Logger.LogDebug(ex, "Failed to resolve shell path {Path}", path);
+ }
+
+ return path.StartsWith(@"\\SHELL\", StringComparison.OrdinalIgnoreCase)
+ ? path.Replace(@"\\SHELL\", string.Empty, StringComparison.OrdinalIgnoreCase)
+ : path;
+ }
+
+ private async Task GetPinnedFolderPathsAsync()
+ {
+ return (await GetPinnedFoldersAsync())
+ .Where(link => (bool?)link.Properties["System.Home.IsPinned"] ?? false)
+ .Select(link => link.FilePath)
+ .ToArray();
+ }
+
+ private async Task GetMissingPinnedItemsAsync(IEnumerable desiredItems)
+ {
+ var normalizedCurrentPinned = BuildNormalizedPathSet(await GetPinnedFolderPathsAsync());
+ return desiredItems
+ .Where(x => !normalizedCurrentPinned.Contains(NormalizeQuickAccessPath(x)))
+ .ToArray();
+ }
+
+ private static async Task WaitUntilAsync(Func> condition, TimeSpan timeout)
+ {
+ if (await condition())
+ return true;
+
+ // Quick Access state is saved by the OS into f01b...automaticDestinations-ms
+ var automaticDestinationsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Microsoft", "Windows", "Recent", "AutomaticDestinations");
+
+ if (!Directory.Exists(automaticDestinationsPath))
+ return await PollWaitAsync(condition, timeout, TimeSpan.FromMilliseconds(200));
+
+ using var cts = new CancellationTokenSource(timeout);
+ using var watcher = new FileSystemWatcher(automaticDestinationsPath, "f01b4d95cf55d32a.automaticDestinations-ms")
+ {
+ NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName
+ };
+
+ using var semaphore = new SemaphoreSlim(0);
+ void OnChanged(object sender, FileSystemEventArgs e)
+ {
+ try
+ {
+ semaphore.Release();
+ }
+ catch (ObjectDisposedException)
+ {
+
+ }
+ }
+
+ watcher.Changed += OnChanged;
+ watcher.Created += OnChanged;
+ watcher.Deleted += OnChanged;
+
+ try
+ {
+ watcher.EnableRaisingEvents = true;
+
+ while (!cts.IsCancellationRequested)
+ {
+ if (await condition())
+ return true;
+
+ try
+ {
+ // prevents wait deadlocks if the FileSystemWatcher
+ // randomly swallows a background COM completion event
+ await semaphore.WaitAsync(TimeSpan.FromMilliseconds(400), cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+ }
+ finally
+ {
+ watcher.Changed -= OnChanged;
+ watcher.Created -= OnChanged;
+ watcher.Deleted -= OnChanged;
+ }
+
+ return await condition();
+ }
+
+ private static async Task PollWaitAsync(Func> condition, TimeSpan timeout, TimeSpan pollInterval)
+ {
+ using var cts = new CancellationTokenSource(timeout);
+ while (!cts.IsCancellationRequested)
+ {
+ if (await condition())
+ return true;
+
+ try
+ {
+ await Task.Delay(pollInterval, cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ break;
+ }
+ }
+
+ return await condition();
+ }
+
+ private async Task ReconcilePinsAsync(string[] desiredItems)
+ {
+ await WaitUntilAsync(async () =>
+ {
+ var missingItems = await GetMissingPinnedItemsAsync(desiredItems);
+ if (missingItems.Length == 0)
+ return true;
+
+ await PinToSidebarAsync(missingItems, false, force: true);
+ return false;
+ }, ReconciliationTimeout);
}
public async Task SaveAsync(string[] items)
{
- if (Equals(items, App.QuickAccessManager.Model.PinnedFolders.ToArray()))
+ var desiredItems = items
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ if (desiredItems.SequenceEqual(App.QuickAccessManager.Model.PinnedFolders, StringComparer.OrdinalIgnoreCase))
return;
if (App.QuickAccessManager.PinnedItemsWatcher is not null)
App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = false;
- // Unpin every item that is below this index and then pin them all in order
- await UnpinFromSidebarAsync([], false);
+ try
+ {
+ var itemsToUnpin = await GetPinnedFolderPathsAsync();
- await PinToSidebarAsync(items, false);
- if (App.QuickAccessManager.PinnedItemsWatcher is not null)
- App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true;
+ if (itemsToUnpin.Length > 0)
+ {
+ await UnpinFromSidebarAsync(itemsToUnpin, false);
+ await WaitUntilAsync(async () =>
+ {
+ var currentPinned = await GetPinnedFolderPathsAsync();
+ var normalizedCurrentPinned = BuildNormalizedPathSet(currentPinned);
- App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(items, true)
+ return !itemsToUnpin.Any(x => normalizedCurrentPinned.Contains(NormalizeQuickAccessPath(x)));
+ }, UnpinSettleTimeout);
+ }
+
+ await ReconcilePinsAsync(desiredItems);
+ await App.QuickAccessManager.Model.LoadAsync();
+ }
+ finally
{
- Reorder = true
- });
+ if (App.QuickAccessManager.PinnedItemsWatcher is not null)
+ App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true;
+ }
}
}
}
diff --git a/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs b/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs
index 528f1e9a0b46..681c6171e259 100644
--- a/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs
+++ b/src/Files.App/Utils/Storage/Collection/BulkConcurrentObservableCollection.cs
@@ -375,6 +375,21 @@ public void RemoveAt(int index)
UpdateGroups(e);
}
+ public void Move(int oldIndex, int newIndex)
+ {
+ NotifyCollectionChangedEventArgs e;
+
+ lock (syncRoot)
+ {
+ var item = collection[oldIndex];
+ collection.RemoveAt(oldIndex);
+ collection.Insert(newIndex, item);
+
+ e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex);
+ OnCollectionChanged(e, false);
+ }
+ }
+
public void AddRange(IEnumerable items)
{
if (!items.Any())
diff --git a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs
index ba9ab3ba408a..7769ac93b724 100644
--- a/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs
+++ b/src/Files.App/Utils/Storage/Helpers/StorageFileExtensions.cs
@@ -74,7 +74,7 @@ public static bool AreItemsAlreadyInFolder(this IEnumerable itemsPath, s
try
{
var trimmedPath = destinationPath.TrimPath();
- return itemsPath.All(itemPath => Path.GetDirectoryName(itemPath).Equals(trimmedPath, StringComparison.OrdinalIgnoreCase));
+ return itemsPath.All(itemPath => Path.GetDirectoryName(itemPath)?.Equals(trimmedPath, StringComparison.OrdinalIgnoreCase) == true);
}
catch
{
diff --git a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
index 5ca29d830913..c15b1e6f98f5 100644
--- a/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
+++ b/src/Files.App/Utils/Storage/Operations/FilesystemHelpers.cs
@@ -276,7 +276,14 @@ public async Task PerformOperationTypeAsync(
}
finally
{
- packageView.ReportOperationCompleted(packageView.RequestedOperation);
+ try
+ {
+ packageView.ReportOperationCompleted(packageView.RequestedOperation);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogInformation(ex, "Drag data package became unavailable while reporting the completed operation");
+ }
}
}
@@ -728,15 +735,19 @@ await Ioc.Default.GetRequiredService().TryGetFileAsync(dest)
public static bool HasDraggedStorageItems(DataPackageView packageView)
{
- return packageView is not null && (packageView.Contains(StandardDataFormats.StorageItems) || packageView.Contains("FileDrop"));
+ if (packageView is null)
+ return false;
+
+ return packageView.Contains(StandardDataFormats.StorageItems) || packageView.Contains("FileDrop");
}
public static async Task> GetDraggedStorageItems(DataPackageView packageView)
{
var itemsList = new List();
var hasVirtualItems = false;
+ bool containsStorageItems = packageView.Contains(StandardDataFormats.StorageItems);
- if (packageView.Contains(StandardDataFormats.StorageItems))
+ if (containsStorageItems)
{
try
{
@@ -757,7 +768,11 @@ public static async Task> GetDraggedStorageIte
// workaround for pasting folders from remote desktop (#12318)
try
{
- if (hasVirtualItems && packageView.Contains("FileContents"))
+ var containsFileContents = false;
+ if (hasVirtualItems)
+ containsFileContents = packageView.Contains("FileContents");
+
+ if (hasVirtualItems && containsFileContents)
{
var descriptor = NativeClipboard.CurrentDataObject.GetData("FileGroupDescriptorW");
for (var ii = 0; ii < descriptor.cItems; ii++)
@@ -779,7 +794,9 @@ public static async Task> GetDraggedStorageIte
// workaround for GetStorageItemsAsync() bug that only yields 16 items at most
// https://learn.microsoft.com/windows/win32/shell/clipboard#cf_hdrop
- if (packageView.Contains("FileDrop"))
+ bool containsFileDrop = packageView.Contains("FileDrop");
+
+ if (containsFileDrop)
{
var fileDropData = await SafetyExtensions.IgnoreExceptions(
() => packageView.GetDataAsync("FileDrop").AsTask());
diff --git a/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs
deleted file mode 100644
index f4f90ffc4ba1..000000000000
--- a/src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) Files Community
-// Licensed under the MIT License.
-
-using System.Windows.Input;
-
-namespace Files.App.ViewModels.Dialogs
-{
- public sealed partial class ReorderSidebarItemsDialogViewModel : ObservableObject
- {
- private readonly IQuickAccessService quickAccessService = Ioc.Default.GetRequiredService();
-
- public string HeaderText = Strings.ReorderSidebarItemsDialogText.GetLocalizedResource();
- public ICommand PrimaryButtonCommand { get; private set; }
-
- public ObservableCollection SidebarPinnedFolderItems = new(App.QuickAccessManager.Model._PinnedFolderItems
- .Where(x => x is LocationItem loc && loc.Section is SectionType.Pinned && !loc.IsHeader)
- .Cast());
-
- public ReorderSidebarItemsDialogViewModel()
- {
- //App.Logger.LogWarning(string.Join(", ", SidebarPinnedFolderItems.Select(x => x.Path)));
- PrimaryButtonCommand = new RelayCommand(SaveChanges);
- }
-
- public void SaveChanges()
- {
- quickAccessService.SaveAsync(SidebarPinnedFolderItems.Select(x => x.Path).ToArray());
- }
- }
-}
diff --git a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
index d3f9f314479a..81c2e30f5cbe 100644
--- a/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
@@ -3,6 +3,7 @@
using Files.App.Controls;
using Files.App.Helpers.ContextFlyouts;
+using Microsoft.Extensions.Logging;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -47,6 +48,8 @@ public IFilesystemHelpers FilesystemHelpers
public PinnedFoldersManager SidebarPinnedModel => App.QuickAccessManager.Model;
public IQuickAccessService QuickAccessService { get; } = Ioc.Default.GetRequiredService();
+ private bool isReordering = false;
+
private SidebarDisplayMode sidebarDisplayMode;
public SidebarDisplayMode SidebarDisplayMode
{
@@ -267,7 +270,6 @@ public SidebarViewModel()
PinItemCommand = new RelayCommand(PinItem);
EjectDeviceCommand = new RelayCommand(EjectDevice);
OpenPropertiesCommand = new RelayCommand(OpenProperties);
- ReorderItemsCommand = new AsyncRelayCommand(ReorderItemsAsync);
}
private Task CreateItemHomeAsync()
@@ -282,20 +284,27 @@ private async void Manager_DataChanged(object sender, NotifyCollectionChangedEve
await dispatcherQueue.EnqueueOrInvokeAsync(async () =>
{
- var sectionType = (SectionType)sender;
- var section = await GetOrCreateSectionAsync(sectionType);
- Func> getElements = () => sectionType switch
+ try
{
- SectionType.Pinned => App.QuickAccessManager.Model.PinnedFolderItems,
- SectionType.CloudDrives => CloudDrivesManager.Drives,
- SectionType.Drives => drivesViewModel.Drives.Cast().ToList().AsReadOnly(),
- SectionType.Network => NetworkService.Computers.Cast().ToList().AsReadOnly(),
- SectionType.WSL => WSLDistroManager.Distros,
- SectionType.Library => App.LibraryManager.Libraries,
- SectionType.FileTag => App.FileTagsManager.FileTags,
- _ => null
- };
- await SyncSidebarItemsAsync(section, getElements, e);
+ var sectionType = (SectionType)sender;
+ var section = await GetOrCreateSectionAsync(sectionType);
+ Func> getElements = () => sectionType switch
+ {
+ SectionType.Pinned => App.QuickAccessManager.Model.PinnedFolderItems,
+ SectionType.CloudDrives => CloudDrivesManager.Drives,
+ SectionType.Drives => drivesViewModel.Drives.Cast().ToList().AsReadOnly(),
+ SectionType.Network => NetworkService.Computers.Cast().ToList().AsReadOnly(),
+ SectionType.WSL => WSLDistroManager.Distros,
+ SectionType.Library => App.LibraryManager.Libraries,
+ SectionType.FileTag => App.FileTagsManager.FileTags,
+ _ => null
+ };
+ await SyncSidebarItemsAsync(section, getElements, e);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogWarning(ex, "Error syncing sidebar items");
+ }
});
}
@@ -324,6 +333,29 @@ private async Task SyncSidebarItemsAsync(LocationItem section, Func x.Path == item.Path);
+ if (match is not null)
+ {
+ section.ChildItems.Remove(match);
+ var newIndex = e.NewStartingIndex < 0 ? section.ChildItems.Count : Math.Min(e.NewStartingIndex, section.ChildItems.Count);
+ section.ChildItems.Insert(newIndex, match);
+ }
+ return;
+ }
+
+ // fallback
+ section.ChildItems.Clear();
+ foreach (INavigationControlItem elem in getElements())
+ {
+ await AddElementToSectionAsync(elem, section);
+ }
+ return;
+ }
+
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
{
@@ -407,8 +439,6 @@ await lib.CheckDefaultSaveFolderAccess() &&
section.ChildItems.Insert(index < 0 ? section.ChildItems.Count : Math.Min(index, section.ChildItems.Count), elem);
}
}
-
- section.PropertyChanged += Section_PropertyChanged;
}
private void Section_PropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -586,6 +616,11 @@ private async Task CreateSectionAsync(SectionType sectionType)
}
}
+ if (section is not null)
+ {
+ section.PropertyChanged += Section_PropertyChanged;
+ }
+
return section;
}
@@ -866,8 +901,6 @@ public async void HandleItemInvokedAsync(object item, PointerUpdateKind pointerU
private ICommand OpenPropertiesCommand { get; }
- private ICommand ReorderItemsCommand { get; }
-
private void PinItem()
{
if (rightClickedItem is DriveItem)
@@ -907,13 +940,6 @@ private void HideSection()
}
}
- private async Task ReorderItemsAsync()
- {
- var dialog = new ReorderSidebarItemsDialogViewModel();
- var dialogService = Ioc.Default.GetRequiredService();
- var result = await dialogService.ShowDialogAsync(dialog);
- }
-
private void OpenProperties(CommandBarFlyout menu)
{
EventHandler