Skip to content
13 changes: 13 additions & 0 deletions src/Files.App.Controls/Sidebar/ISidebarItemModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,17 @@ public interface ISidebarItemModel : INotifyPropertyChanged
/// </summary>
string? Path { get; }
}

public interface IDraggableSidebarItemModel : ISidebarItemModel
{
/// <summary>
/// The file path used for drag and drop operations
/// </summary>
string? DropPath { get; }

/// <summary>
/// Indicates whether the item supports reorder dropping
/// </summary>
bool IsReorderDropItem { get; }
}
}
107 changes: 77 additions & 30 deletions src/Files.App.Controls/Sidebar/SidebarItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@

using CommunityToolkit.WinUI;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Runtime.InteropServices;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;

Expand Down Expand Up @@ -92,7 +97,17 @@ public void HandleItemChange()
HookupItemChangeListener(null, Item);
UpdateExpansionState();
ReevaluateSelection();
CanDrag = Item?.Path 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()
Expand Down Expand Up @@ -138,32 +153,36 @@ 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?.Path 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 =>
SafetyExtensions.IgnoreExceptions(() =>
{
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 = SafetyExtensions.IgnoreExceptions(() => request.GetDeferral(), null, typeof(COMException));
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();
}
});
finally
{
if (deferral is not null)
SafetyExtensions.IgnoreExceptions(() => deferral.Complete(), null, typeof(COMException));
}
});
}, null, typeof(COMException));
}

private void SetFlyoutOpen(bool isOpen = true)
Expand Down Expand Up @@ -394,21 +413,49 @@ private async void ItemBorder_DragOver(object sender, DragEventArgs e)
IsExpanded = true;
}

var insertsAbove = DetermineDropTargetPosition(e);
if (insertsAbove == SidebarItemDropPosition.Center)
{
VisualStateManager.GoToState(this, "DragOnTop", true);
}
else if (insertsAbove == SidebarItemDropPosition.Top)
// Expected to fail with COMException if the OLE drag payload is stale
var deferral = SafetyExtensions.IgnoreExceptions(() => e.GetDeferral(), null, typeof(COMException));

try
{
VisualStateManager.GoToState(this, "DragInsertAbove", true);
var dropPosition = DetermineDropTargetPosition(e);

if (Owner is not null)
Owner.RaiseItemDragOver(this, dropPosition, e);

bool isHandled = false;
DataPackageOperation acceptedOperation = DataPackageOperation.None;

var propertiesRead = SafetyExtensions.IgnoreExceptions(() =>
{
isHandled = e.Handled;
acceptedOperation = e.AcceptedOperation;
}, null, typeof(COMException));

if (!propertiesRead || !isHandled || 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);
}
}
else if (insertsAbove == SidebarItemDropPosition.Bottom)
finally
{
VisualStateManager.GoToState(this, "DragInsertBelow", true);
if (deferral is not null)
SafetyExtensions.IgnoreExceptions(() => deferral.Complete(), null, typeof(COMException));
}

Owner?.RaiseItemDragOver(this, insertsAbove, e);
}

private void ItemBorder_ContextRequested(UIElement sender, Microsoft.UI.Xaml.Input.ContextRequestedEventArgs args)
Expand Down
17 changes: 15 additions & 2 deletions src/Files.App.Controls/Sidebar/SidebarView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
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 System.Runtime.InteropServices;
using Windows.UI.Core;

namespace Files.App.Controls
Expand Down Expand Up @@ -51,13 +53,24 @@ 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));

// Expected to fail with COMException if the OLE drag payload is stale
var dataView = SafetyExtensions.IgnoreExceptions(() => rawEvent.DataView, null, typeof(COMException));
if (dataView is null) return;

ItemDropped?.Invoke(this, new(sideBarItem.Item, dataView, dropPosition, rawEvent));
}

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

// Expected to fail with COMException if the OLE drag payload is stale
var dataView = SafetyExtensions.IgnoreExceptions(() => rawEvent.DataView, null, typeof(COMException));
if (dataView is null) return;

var args = new ItemDragOverEventArgs(sideBarItem.Item, dataView, dropPosition, rawEvent);
ItemDragOver?.Invoke(this, args);
}

private void UpdateMinimalMode()
Expand Down
6 changes: 5 additions & 1 deletion src/Files.App/Data/Contracts/INavigationControlItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@

namespace Files.App.Data.Contracts
{
public interface INavigationControlItem : IComparable<INavigationControlItem>, INotifyPropertyChanged, ISidebarItemModel
public interface INavigationControlItem : IComparable<INavigationControlItem>, 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; }
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Data/Items/LocationItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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" ||
Expand Down
Loading
Loading