Skip to content

Commit abd09bc

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

File tree

14 files changed

+699
-95
lines changed

14 files changed

+699
-95
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: 110 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 = draggableItem.DropPath is not null && System.IO.Path.IsPathRooted(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()
@@ -140,30 +150,48 @@ private void HookupItemChangeListener(ISidebarItemModel? oldItem, ISidebarItemMo
140150

141151
private void SidebarItem_DragStarting(UIElement sender, DragStartingEventArgs args)
142152
{
143-
if (Item?.GetType().GetProperty("Path")?.GetValue(Item) is not string dragPath || !Path.IsPathRooted(dragPath))
153+
if (Item is not IDraggableSidebarItemModel draggableItem || draggableItem.DropPath is not string dragPath || !System.IO.Path.IsPathRooted(dragPath))
144154
return;
145155

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 =>
156+
try
149157
{
150-
var deferral = request.GetDeferral();
151-
try
158+
args.Data.SetData(StandardDataFormats.Text, dragPath);
159+
args.Data.RequestedOperation = DataPackageOperation.Move | DataPackageOperation.Copy | DataPackageOperation.Link;
160+
args.Data.SetDataProvider(StandardDataFormats.StorageItems, async request =>
152161
{
153-
if (Directory.Exists(dragPath))
162+
var deferral = request.GetDeferral();
163+
try
154164
{
155-
var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
156-
request.SetData(new IStorageItem[] { folder });
165+
if (Directory.Exists(dragPath))
166+
{
167+
var folder = await StorageFolder.GetFolderFromPathAsync(dragPath);
168+
request.SetData(new IStorageItem[] { folder });
169+
}
157170
}
158-
}
159-
catch
160-
{
161-
}
162-
finally
163-
{
164-
deferral.Complete();
165-
}
166-
});
171+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
172+
{
173+
// External OLE drag payload became stale while resolving StorageFolder — ignore.
174+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload while resolving StorageFolder in data provider.");
175+
}
176+
finally
177+
{
178+
try
179+
{
180+
deferral.Complete();
181+
}
182+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
183+
{
184+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral during drag data provider completion.");
185+
}
186+
}
187+
});
188+
}
189+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
190+
{
191+
// OLE channel was already closed before drag started
192+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload on DragStarting, cancelling drag.");
193+
args.Cancel = true;
194+
}
167195
}
168196

169197
private void SetFlyoutOpen(bool isOpen = true)
@@ -394,21 +422,64 @@ private async void ItemBorder_DragOver(object sender, DragEventArgs e)
394422
IsExpanded = true;
395423
}
396424

397-
var insertsAbove = DetermineDropTargetPosition(e);
398-
if (insertsAbove == SidebarItemDropPosition.Center)
425+
DragOperationDeferral? deferral = null;
426+
try
399427
{
400-
VisualStateManager.GoToState(this, "DragOnTop", true);
428+
deferral = e.GetDeferral();
401429
}
402-
else if (insertsAbove == SidebarItemDropPosition.Top)
430+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
403431
{
404-
VisualStateManager.GoToState(this, "DragInsertAbove", true);
432+
// OLE pipeline already torn down before DragOver deferral was obtained — abort gracefully.
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);
441+
442+
if (Owner is not null)
443+
await Owner.RaiseItemDragOverAsync(this, insertsAbove, e);
410444

411-
Owner?.RaiseItemDragOver(this, insertsAbove, e);
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+
// External OLE drag payload became stale during DragOver processing
467+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar DragOver processing.");
468+
e.AcceptedOperation = DataPackageOperation.None;
469+
VisualStateManager.GoToState(this, "Normal", true);
470+
}
471+
finally
472+
{
473+
try
474+
{
475+
deferral?.Complete();
476+
}
477+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
478+
{
479+
// Deferral completion failed because OLE channel was torn down — ignore.
480+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE deferral on DragOver completion.");
481+
}
482+
}
412483
}
413484

414485
private void ItemBorder_ContextRequested(UIElement sender, Microsoft.UI.Xaml.Input.ContextRequestedEventArgs args)
@@ -425,7 +496,17 @@ private void ItemBorder_DragLeave(object sender, DragEventArgs e)
425496
private void ItemBorder_Drop(object sender, DragEventArgs e)
426497
{
427498
UpdatePointerState();
428-
Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
499+
try
500+
{
501+
Owner?.RaiseItemDropped(this, DetermineDropTargetPosition(e), e);
502+
}
503+
catch (Exception ex) when (DragDropExceptionHelper.IsExpectedStaleDragData(ex))
504+
{
505+
// External OLE drag payload became stale on drop — no reorder commit, reset state.
506+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale external drag payload during sidebar Drop — drop discarded.");
507+
e.AcceptedOperation = DataPackageOperation.None;
508+
e.Handled = true;
509+
}
429510
}
430511

431512
private SidebarItemDropPosition DetermineDropTargetPosition(DragEventArgs args)

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

Lines changed: 28 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,37 @@ 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+
// DataView access failed, external OLE payload is already stale
65+
DragDropExceptionHelper.LogStaleDrag(ex, "Stale OLE drag payload reading DataView in RaiseItemDropped.");
66+
return;
67+
}
68+
ItemDropped?.Invoke(this, new(sideBarItem.Item, dataView, dropPosition, rawEvent));
5769
}
5870

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

6590
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)