Skip to content

Commit fff0cd5

Browse files
committed
Feature: Enable drag and drop reordering for Pinned Sidebar items
1 parent d2e392a commit fff0cd5

18 files changed

+709
-297
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using System.Diagnostics;
5+
using System.Runtime.InteropServices;
6+
7+
namespace Files.App.Controls
8+
{
9+
/// <summary>
10+
/// Provides helper methods for classifying expected drag-and-drop COM failures
11+
/// caused by stale OLE drag payloads (e.g. from Windows Explorer).
12+
/// </summary>
13+
internal static class DragDropExceptionHelper
14+
{
15+
// CLIPBRD_E_CANT_OPEN / OLE_E_NOTRUNNING: clipboard/data object is no longer available
16+
private const int HRESULT_CLIPBOARD_DATA_UNAVAILABLE = unchecked((int)0x800401D0);
17+
18+
// RPC_E_SERVERFAULT: OLE/RPC drag pipeline failure (stale cross-process drag)
19+
private const int HRESULT_RPC_OLE_FAILURE = unchecked((int)0x80010105);
20+
21+
/// <summary>
22+
/// Returns <see langword="true"/> when <paramref name="ex"/> is a <see cref="COMException"/>
23+
/// with an HResult that indicates a stale or already-released OLE drag payload.
24+
/// These are expected during sidebar reorder when the user also has File Explorer open.
25+
/// </summary>
26+
public static bool IsExpectedStaleDragData(Exception ex)
27+
{
28+
return ex is COMException com &&
29+
(com.HResult == HRESULT_CLIPBOARD_DATA_UNAVAILABLE ||
30+
com.HResult == HRESULT_RPC_OLE_FAILURE);
31+
}
32+
33+
/// <summary>
34+
/// Writes a debug-level trace for a stale drag payload event.
35+
/// </summary>
36+
[Conditional("DEBUG")]
37+
public static void LogStaleDrag(Exception ex, string message)
38+
{
39+
Debug.WriteLine($"[DragDrop] {message} HResult=0x{ex.HResult:X8}");
40+
}
41+
}
42+
}

src/Files.App.Controls/Sidebar/ISidebarItemModel.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,17 @@ public interface ISidebarItemModel : INotifyPropertyChanged
2121
/// </summary>
2222
bool PaddedItem { get; }
2323
}
24+
25+
public interface IDraggableSidebarItemModel : ISidebarItemModel
26+
{
27+
/// <summary>
28+
/// The file path used for drag and drop operations
29+
/// </summary>
30+
string? DropPath { get; }
31+
32+
/// <summary>
33+
/// Indicates whether the item supports reorder dropping
34+
/// </summary>
35+
bool IsReorderDropItem { get; }
36+
}
2437
}

src/Files.App.Controls/Sidebar/ISidebarViewModel.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ namespace Files.App.Controls
99
{
1010
public record ItemInvokedEventArgs(PointerUpdateKind PointerUpdateKind) { }
1111
public record ItemDroppedEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent) { }
12-
public record ItemDragOverEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent) { }
12+
public record ItemDragOverEventArgs(object DropTarget, DataPackageView DroppedItem, SidebarItemDropPosition dropPosition, DragEventArgs RawEvent)
13+
{
14+
/// <summary>
15+
/// Set by the event handler to signal async completion
16+
/// </summary>
17+
public Task? CompletionTask { get; set; }
18+
}
1319
public record ItemContextInvokedArgs(object? Item, Point Position) { }
1420
}

src/Files.App.Controls/Sidebar/SidebarItem.cs

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,17 @@ public void HandleItemChange()
9292
HookupItemChangeListener(null, Item);
9393
UpdateExpansionState();
9494
ReevaluateSelection();
95-
CanDrag = Item?.GetType().GetProperty("Path")?.GetValue(Item) is string path && Path.IsPathRooted(path);
95+
96+
if (Item is IDraggableSidebarItemModel draggableItem)
97+
{
98+
CanDrag = IsValidDropPath(draggableItem.DropPath);
99+
UseReorderDrop = !IsGroupHeader && CanDrag && draggableItem.IsReorderDropItem;
100+
}
101+
else
102+
{
103+
CanDrag = false;
104+
UseReorderDrop = false;
105+
}
96106
}
97107

98108
private void HookupOwners()
@@ -138,32 +148,51 @@ private void HookupItemChangeListener(ISidebarItemModel? oldItem, ISidebarItemMo
138148
}
139149
}
140150

151+
private static bool IsValidDropPath(string? path)
152+
=> path is not null && (System.IO.Path.IsPathRooted(path) || path.StartsWith("Shell:", StringComparison.OrdinalIgnoreCase));
153+
141154
private void SidebarItem_DragStarting(UIElement sender, DragStartingEventArgs args)
142155
{
143-
if (Item?.GetType().GetProperty("Path")?.GetValue(Item) is not string dragPath || !Path.IsPathRooted(dragPath))
156+
if (Item is not IDraggableSidebarItemModel draggableItem || draggableItem.DropPath is not string dragPath || !IsValidDropPath(dragPath))
144157
return;
145158

146-
args.Data.SetData(StandardDataFormats.Text, dragPath);
147-
args.Data.RequestedOperation = DataPackageOperation.Move | DataPackageOperation.Copy | DataPackageOperation.Link;
148-
args.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
159+
try
149160
{
150-
var deferral = request.GetDeferral();
151-
try
161+
args.Data.SetData(StandardDataFormats.Text, dragPath);
162+
args.Data.RequestedOperation = DataPackageOperation.Move | DataPackageOperation.Copy | DataPackageOperation.Link;
163+
args.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
152164
{
153-
if (Directory.Exists(dragPath))
165+
var deferral = request.GetDeferral();
166+
try
154167
{
155-
var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
156-
request.SetData(new IStorageItem[] { folder });
168+
if (Directory.Exists(dragPath))
169+
{
170+
var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
171+
request.SetData(new IStorageItem[] { folder });
172+
}
157173
}
158-
}
159-
catch
160-
{
161-
}
162-
finally
163-
{
164-
deferral.Complete();
165-
}
166-
});
174+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
175+
{
176+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload while resolving StorageFolder in data provider.");
177+
}
178+
finally
179+
{
180+
try
181+
{
182+
deferral.Complete();
183+
}
184+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
185+
{
186+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral during drag data provider completion.");
187+
}
188+
}
189+
});
190+
}
191+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
192+
{
193+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload on DragStarting, cancelling drag.");
194+
args.Cancel = true;
195+
}
167196
}
168197

169198
private void SetFlyoutOpen(bool isOpen = true)
@@ -394,21 +423,61 @@ private async void ItemBorder_DragOver(object sender, DragEventArgs e)
394423
IsExpanded = true;
395424
}
396425

397-
var insertsAbove = DetermineDropTargetPosition(e);
398-
if (insertsAbove == SidebarItemDropPosition.Center)
426+
DragOperationDeferral? deferral = null;
427+
try
399428
{
400-
VisualStateManager.GoToState(this, "DragOnTop", true);
429+
deferral = e.GetDeferral();
401430
}
402-
else if (insertsAbove == SidebarItemDropPosition.Top)
431+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
403432
{
404-
VisualStateManager.GoToState(this, "DragInsertAbove", true);
433+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload on GetDeferral during DragOver.");
434+
VisualStateManager.GoToState(this, "Normal", true);
435+
return;
405436
}
406-
else if (insertsAbove == SidebarItemDropPosition.Bottom)
437+
438+
try
407439
{
408-
VisualStateManager.GoToState(this, "DragInsertBelow", true);
409-
}
440+
var insertsAbove = DetermineDropTargetPosition(e);
410441

411-
Owner?.RaiseItemDragOver(this, insertsAbove, e);
442+
if (Owner is not null)
443+
await Owner.RaiseItemDragOverAsync(this, insertsAbove, e);
444+
445+
if (!e.Handled || e.AcceptedOperation == DataPackageOperation.None)
446+
{
447+
VisualStateManager.GoToState(this, "Normal", true);
448+
return;
449+
}
450+
451+
if (insertsAbove == SidebarItemDropPosition.Center)
452+
{
453+
VisualStateManager.GoToState(this, "DragOnTop", true);
454+
}
455+
else if (insertsAbove == SidebarItemDropPosition.Top)
456+
{
457+
VisualStateManager.GoToState(this, "DragInsertAbove", true);
458+
}
459+
else if (insertsAbove == SidebarItemDropPosition.Bottom)
460+
{
461+
VisualStateManager.GoToState(this, "DragInsertBelow", true);
462+
}
463+
}
464+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
465+
{
466+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar DragOver processing.");
467+
e.AcceptedOperation = DataPackageOperation.None;
468+
VisualStateManager.GoToState(this, "Normal", true);
469+
}
470+
finally
471+
{
472+
try
473+
{
474+
deferral?.Complete();
475+
}
476+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
477+
{
478+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral on DragOver completion.");
479+
}
480+
}
412481
}
413482

414483
private void ItemBorder_ContextRequested(UIElement sender, Microsoft.UI.Xaml.Input.ContextRequestedEventArgs args)
@@ -425,7 +494,16 @@ private void ItemBorder_DragLeave(object sender, DragEventArgs e)
425494
private void ItemBorder_Drop(object sender, DragEventArgs e)
426495
{
427496
UpdatePointerState();
428-
Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
497+
try
498+
{
499+
Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
500+
}
501+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
502+
{
503+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar Drop, drop discarded.");
504+
e.AcceptedOperation = DataPackageOperation.None;
505+
e.Handled = true;
506+
}
429507
}
430508

431509
private SidebarItemDropPosition DetermineDropTargetPosition(DragEventArgs args)

src/Files.App.Controls/Sidebar/SidebarView.xaml.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.UI.Input;
55
using Microsoft.UI.Xaml.Input;
66
using Microsoft.UI.Xaml.Markup;
7+
using Windows.ApplicationModel.DataTransfer;
78
using Windows.Foundation;
89
using Windows.System;
910
using Windows.UI.Core;
@@ -53,13 +54,36 @@ internal void RaiseContextRequested(SidebarItem item, Point e)
5354
internal void RaiseItemDropped(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
5455
{
5556
if (sideBarItem.Item is null) return;
56-
ItemDropped?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
57+
DataPackageView dataView;
58+
try
59+
{
60+
dataView = rawEvent.DataView;
61+
}
62+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
63+
{
64+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDropped.");
65+
return;
66+
}
67+
ItemDropped?.Invoke(this, new(sideBarItem.Item, dataView, dropPosition, rawEvent));
5768
}
5869

59-
internal void RaiseItemDragOver(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
70+
internal async Task RaiseItemDragOverAsync(SidebarItem sideBarItem, SidebarItemDropPosition dropPosition, DragEventArgs rawEvent)
6071
{
6172
if (sideBarItem.Item is null) return;
62-
ItemDragOver?.Invoke(this, new(sideBarItem.Item, rawEvent.DataView, dropPosition, rawEvent));
73+
DataPackageView dataView;
74+
try
75+
{
76+
dataView = rawEvent.DataView;
77+
}
78+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
79+
{
80+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDragOverAsync.");
81+
return;
82+
}
83+
var args = new ItemDragOverEventArgs(sideBarItem.Item, dataView, dropPosition, rawEvent);
84+
ItemDragOver?.Invoke(this, args);
85+
if (args.CompletionTask is not null)
86+
await args.CompletionTask;
6387
}
6488

6589
private void UpdateMinimalMode()

src/Files.App/Data/Contracts/INavigationControlItem.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55

66
namespace Files.App.Data.Contracts
77
{
8-
public interface INavigationControlItem : IComparable<INavigationControlItem>, INotifyPropertyChanged, ISidebarItemModel
8+
public interface INavigationControlItem : IComparable<INavigationControlItem>, INotifyPropertyChanged, IDraggableSidebarItemModel
99
{
1010
public new string Text { get; }
1111

1212
public string Path { get; }
1313

14+
string? IDraggableSidebarItemModel.DropPath => Path;
15+
16+
bool IDraggableSidebarItemModel.IsReorderDropItem => Section == SectionType.Pinned;
17+
1418
public SectionType Section { get; }
1519

1620
public NavigationControlItemType ItemType { get; }

0 commit comments

Comments
 (0)