From fff0cd5ca094c015ff54501d2fbf1215c241128e Mon Sep 17 00:00:00 2001
From: Kalmix <87293493+kalmix@users.noreply.github.com>
Date: Sun, 12 Apr 2026 15:28:30 -0400
Subject: [PATCH 1/6] Feature: Enable drag and drop reordering for Pinned
Sidebar items
---
.../Sidebar/DragDropExceptionHelper.cs | 42 ++++
.../Sidebar/ISidebarItemModel.cs | 13 ++
.../Sidebar/ISidebarViewModel.cs | 8 +-
src/Files.App.Controls/Sidebar/SidebarItem.cs | 136 +++++++++---
.../Sidebar/SidebarView.xaml.cs | 30 ++-
.../Data/Contracts/INavigationControlItem.cs | 6 +-
.../Data/Models/PinnedFoldersManager.cs | 149 ++++++++++++-
.../Dialogs/ReorderSidebarItemsDialog.xaml | 61 ------
.../Dialogs/ReorderSidebarItemsDialog.xaml.cs | 95 --------
.../Services/App/AppDialogService.cs | 1 -
.../Windows/WindowsQuickAccessService.cs | 11 +-
.../BulkConcurrentObservableCollection.cs | 15 ++
.../Storage/Helpers/StorageFileExtensions.cs | 2 +-
.../Storage/Operations/FilesystemHelpers.cs | 54 ++++-
.../ReorderSidebarItemsDialogViewModel.cs | 30 ---
.../UserControls/SidebarViewModel.cs | 205 +++++++++++++++---
.../Widgets/QuickAccessWidgetViewModel.cs | 84 ++++++-
src/Files.App/Views/MainPage.xaml.cs | 64 +++++-
18 files changed, 709 insertions(+), 297 deletions(-)
create mode 100644 src/Files.App.Controls/Sidebar/DragDropExceptionHelper.cs
delete mode 100644 src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml
delete mode 100644 src/Files.App/Dialogs/ReorderSidebarItemsDialog.xaml.cs
delete mode 100644 src/Files.App/ViewModels/Dialogs/ReorderSidebarItemsDialogViewModel.cs
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/ISidebarViewModel.cs b/src/Files.App.Controls/Sidebar/ISidebarViewModel.cs
index 41d8c037bceb..41a13109cef4 100644
--- a/src/Files.App.Controls/Sidebar/ISidebarViewModel.cs
+++ b/src/Files.App.Controls/Sidebar/ISidebarViewModel.cs
@@ -9,6 +9,12 @@ namespace Files.App.Controls
{
public record ItemInvokedEventArgs(PointerUpdateKind PointerUpdateKind) { }
public record ItemDroppedEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent) { }
- public record ItemDragOverEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent) { }
+ public record ItemDragOverEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent)
+ {
+ ///
+ /// Set by the event handler to signal async completion
+ ///
+ public Task? CompletionTask { get; set; }
+ }
public record ItemContextInvokedArgs(object? Item, Point Position) { }
}
diff --git a/src/Files.App.Controls/Sidebar/SidebarItem.cs b/src/Files.App.Controls/Sidebar/SidebarItem.cs
index ddd0a3df3dc5..0f3a9c4b798a 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 insertsAbove = DetermineDropTargetPosition(e);
- Owner?.RaiseItemDragOver(this, insertsAbove, e);
+ if (Owner is not null)
+ await Owner.RaiseItemDragOverAsync(this, insertsAbove, e);
+
+ if (!e.Handled || e.AcceptedOperation == DataPackageOperation.None)
+ {
+ VisualStateManager.GoToState(this, "Normal", true);
+ return;
+ }
+
+ if (insertsAbove == SidebarItemDropPosition.Center)
+ {
+ VisualStateManager.GoToState(this, "DragOnTop", true);
+ }
+ else if (insertsAbove == SidebarItemDropPosition.Top)
+ {
+ VisualStateManager.GoToState(this, "DragInsertAbove", true);
+ }
+ else if (insertsAbove == 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..d46a8a4a96c5 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,36 @@ 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));
+ DataPackageView dataView;
+ try
+ {
+ dataView = rawEvent.DataView;
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDropped.");
+ return;
+ }
+ ItemDropped?.Invoke(this, new(sideBarItem.Item, dataView, dropPosition, rawEvent));
}
- internal void RaiseItemDragOver(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
+ internal async Task RaiseItemDragOverAsync(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
{
if (sideBarItem.Item is null) return;
- ItemDragOver?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
+ DataPackageView dataView;
+ try
+ {
+ dataView = rawEvent.DataView;
+ }
+ catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
+ {
+ DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDragOverAsync.");
+ return;
+ }
+ var args = new ItemDragOverEventArgs(sideBarItem.Item, dataView, dropPosition, rawEvent);
+ ItemDragOver?.Invoke(this, args);
+ if (args.CompletionTask is not null)
+ await args.CompletionTask;
}
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/Models/PinnedFoldersManager.cs b/src/Files.App/Data/Models/PinnedFoldersManager.cs
index 33eef2120c63..f65978f5c3a9 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,83 @@ 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 baseIndex = _PinnedFolderItems.FindIndex(x => x is LocationItem item && !item.IsDefaultLocation);
+ if (baseIndex == -1)
+ {
+ baseIndex = _PinnedFolderItems.FindLastIndex(x => x is LocationItem item && item.IsDefaultLocation);
+ baseIndex = baseIndex == -1 ? 0 : baseIndex + 1;
+ }
+ return baseIndex;
+ }
+
///
/// Returns the index of the location item in the navigation sidebar
///
@@ -198,8 +323,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 +334,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..1bc4202c7720 100644
--- a/src/Files.App/Services/Windows/WindowsQuickAccessService.cs
+++ b/src/Files.App/Services/Windows/WindowsQuickAccessService.cs
@@ -20,13 +20,13 @@ 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))
+ if (force || !IsItemPinned(folderPath))
await ContextMenu.InvokeVerb("pintohome", folderPath);
}
@@ -101,14 +101,9 @@ public async Task SaveAsync(string[] items)
// Unpin every item that is below this index and then pin them all in order
await UnpinFromSidebarAsync([], false);
- await PinToSidebarAsync(items, false);
+ await PinToSidebarAsync(items, false, force: true);
if (App.QuickAccessManager.PinnedItemsWatcher is not null)
App.QuickAccessManager.PinnedItemsWatcher.EnableRaisingEvents = true;
-
- App.QuickAccessManager.UpdateQuickAccessWidget?.Invoke(this, new ModifyQuickAccessEventArgs(items, true)
- {
- Reorder = 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..32540fcf6c00 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,37 @@ 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;
+
+ try
+ {
+ return packageView.Contains(StandardDataFormats.StorageItems) || packageView.Contains("FileDrop");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogInformation(ex, "Drag data package became unavailable while checking storage items");
+ return false;
+ }
}
public static async Task> GetDraggedStorageItems(DataPackageView packageView)
{
var itemsList = new List();
var hasVirtualItems = false;
+ bool containsStorageItems;
+
+ try
+ {
+ containsStorageItems = packageView.Contains(StandardDataFormats.StorageItems);
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogInformation(ex, "Drag data package became unavailable while enumerating storage items");
+ return itemsList;
+ }
- if (packageView.Contains(StandardDataFormats.StorageItems))
+ if (containsStorageItems)
{
try
{
@@ -757,7 +786,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 +812,18 @@ 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;
+ try
+ {
+ containsFileDrop = packageView.Contains("FileDrop");
+ }
+ catch (Exception ex)
+ {
+ App.Logger.LogInformation(ex, "Drag data package became unavailable while reading file-drop data");
+ return itemsList;
+ }
+
+ 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..047f1680fa91 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 readonly SemaphoreSlim reorderSemaphore = new(1, 1);
+
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()
@@ -280,23 +282,30 @@ private async void Manager_DataChanged(object sender, NotifyCollectionChangedEve
if (dispatcherQueue is null)
return;
- await dispatcherQueue.EnqueueOrInvokeAsync(async () =>
+ try
{
- var sectionType = (SectionType)sender;
- var section = await GetOrCreateSectionAsync(sectionType);
- Func> getElements = () => sectionType switch
+ await dispatcherQueue.EnqueueOrInvokeAsync(async () =>
{
- 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");
+ }
}
private void Manager_DataChangedForDrives(object? sender, NotifyCollectionChangedEventArgs e) => Manager_DataChanged(SectionType.Drives, e);
@@ -324,6 +333,26 @@ private async Task SyncSidebarItemsAsync(LocationItem section, Func x.Path == itemToMove?.Path);
+ if (match != null)
+ {
+ var oldIndex = section.ChildItems.IndexOf(match);
+ var newIndex = e.NewStartingIndex;
+
+ if (newIndex >= section.ChildItems.Count)
+ newIndex = section.ChildItems.Count - 1;
+
+ if (newIndex >= 0 && oldIndex >= 0 && oldIndex != newIndex)
+ section.ChildItems.Move(oldIndex, newIndex);
+ }
+ }
+ break;
+ }
+
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Replace:
{
@@ -866,8 +895,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 +934,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