-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Feature: Enable drag and drop reordering for Pinned Sidebar items #18320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
fff0cd5
d96746e
8bb1de2
4ca2152
189b662
254b52e
c9908d1
3b1d37e
89fc12a
512e81c
5c5e1f1
1cc3475
1a16f57
6284a1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| // Copyright (c) Files Community | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Diagnostics; | ||
| using System.Runtime.InteropServices; | ||
|
|
||
| namespace Files.App.Controls | ||
| { | ||
| /// <summary> | ||
| /// Provides helper methods for classifying expected drag-and-drop COM failures | ||
| /// caused by stale OLE drag payloads (e.g. from Windows Explorer). | ||
| /// </summary> | ||
| 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); | ||
|
|
||
| /// <summary> | ||
| /// Returns <see langword="true"/> when <paramref name="ex"/> is a <see cref="COMException"/> | ||
| /// 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. | ||
| /// </summary> | ||
| public static bool IsExpectedStaleDragData(Exception ex) | ||
| { | ||
| return ex is COMException com && | ||
| (com.HResult == HRESULT_CLIPBOARD_DATA_UNAVAILABLE || | ||
| com.HResult == HRESULT_RPC_OLE_FAILURE); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Writes a debug-level trace for a stale drag payload event. | ||
| /// </summary> | ||
| [Conditional("DEBUG")] | ||
| public static void LogStaleDrag(Exception ex, string message) | ||
| { | ||
| Debug.WriteLine($"[DragDrop] {message} HResult=0x{ex.HResult:X8}"); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please do not put system specific code inside Files.App.Controls. It should be done within Files.App. Sidebar reordering should work in any other scenario.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @0x5bfa Thanks for the review, I will work on it this week. |
||
|
|
||
| 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) | ||
| await Owner.RaiseItemDragOverAsync(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) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.