Skip to content
Merged
118 changes: 118 additions & 0 deletions src/Files.App/Actions/FileSystem/CopyItemFromHomeAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;
using System.IO;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;

namespace Files.App.Actions
{
[GeneratedRichCommand]
internal sealed partial class CopyItemFromHomeAction : ObservableObject, IAction
{
private readonly IContentPageContext context;
private readonly IHomePageContext HomePageContext;

public string Label
=> Strings.Copy.GetLocalizedResource();

public string Description
=> Strings.CopyItemDescription.GetLocalizedFormatResource(1);

public RichGlyph Glyph
=> new(themedIconStyle: "App.ThemedIcons.Copy");
public bool IsExecutable
=> GetIsExecutable();

public CopyItemFromHomeAction()
{
context = Ioc.Default.GetRequiredService<IContentPageContext>();
HomePageContext = Ioc.Default.GetRequiredService<IHomePageContext>();
}

public async Task ExecuteAsync(object? parameter = null)
{
if (HomePageContext.RightClickedItem is null)
return;

var item = HomePageContext.RightClickedItem;
var itemPath = item.Path;

if (string.IsNullOrEmpty(itemPath))
return;

try
{
var dataPackage = new DataPackage() { RequestedOperation = DataPackageOperation.Copy };
IStorageItem? storageItem = null;

var folderResult = await context.ShellPage?.ShellViewModel?.GetFolderFromPathAsync(itemPath)!;
if (folderResult)
storageItem = folderResult.Result;

if (storageItem is null)
{
await CopyPathFallback(itemPath);
return;
}

if (storageItem is SystemStorageFolder or SystemStorageFile)
{
var standardItems = await new[] { storageItem }.ToStandardStorageItemsAsync();
if (standardItems.Any())
storageItem = standardItems.First();
}

dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
dataPackage.SetStorageItems(new[] { storageItem }, false);

Clipboard.SetContent(dataPackage);
}
catch (Exception ex)
{
if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized)
{
await CopyPathFallback(itemPath);
return;
}

}
}

private bool GetIsExecutable()
{
var item = HomePageContext.RightClickedItem;

return HomePageContext.IsAnyItemRightClicked
&& item is not null
&& !IsNonCopyableLocation(item);
}

private async Task CopyPathFallback(string path)
{
try
{
await FileOperationsHelpers.SetClipboard(new[] { path }, DataPackageOperation.Copy);
}
catch (Exception ex)
{
App.Logger.LogWarning(ex, "Failed to copy path to clipboard.");
}
}

private bool IsNonCopyableLocation(WidgetCardItem item)
{
if (string.IsNullOrEmpty(item.Path))
return true;

var normalizedPath = Constants.UserEnvironmentPaths.ShellPlaces.GetValueOrDefault(
item.Path.ToUpperInvariant(),
item.Path);

return string.Equals(normalizedPath, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedPath, Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedPath, Constants.UserEnvironmentPaths.MyComputerPath, StringComparison.OrdinalIgnoreCase);
}
}
}
121 changes: 121 additions & 0 deletions src/Files.App/Actions/Sidebar/CopyItemFromSidebarAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using Microsoft.Extensions.Logging;
using System.IO;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;

namespace Files.App.Actions
{
[GeneratedRichCommand]
internal sealed partial class CopyItemFromSidebarAction : ObservableObject, IAction
{
private readonly IContentPageContext context;
private readonly ISidebarContext SidebarContext;

public string Label
=> Strings.Copy.GetLocalizedResource();

public string Description
=> Strings.CopyItemDescription.GetLocalizedFormatResource(1);

public RichGlyph Glyph
=> new(themedIconStyle: "App.ThemedIcons.Copy");
public bool IsExecutable
=> GetIsExecutable();

public CopyItemFromSidebarAction()
{
context = Ioc.Default.GetRequiredService<IContentPageContext>();
SidebarContext = Ioc.Default.GetRequiredService<ISidebarContext>();
}

public async Task ExecuteAsync(object? parameter = null)
{
if (SidebarContext.RightClickedItem is null)
return;

var item = SidebarContext.RightClickedItem;
var itemPath = item.Path;

if (string.IsNullOrEmpty(itemPath))
return;

try
{
var dataPackage = new DataPackage() { RequestedOperation = DataPackageOperation.Copy };
IStorageItem? storageItem = null;

var folderResult = await context.ShellPage?.ShellViewModel?.GetFolderFromPathAsync(itemPath)!;
if (folderResult)
storageItem = folderResult.Result;

if (storageItem is null)
{
await CopyPathFallback(itemPath);
return;
}

if (storageItem is SystemStorageFolder or SystemStorageFile)
{
var standardItems = await new[] { storageItem }.ToStandardStorageItemsAsync();
if (standardItems.Any())
storageItem = standardItems.First();
}

dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
dataPackage.SetStorageItems(new[] { storageItem }, false);

Clipboard.SetContent(dataPackage);
}
catch (Exception ex)
{
if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized)
{
await CopyPathFallback(itemPath);
return;
}

}
}

private bool GetIsExecutable()
{
var item = SidebarContext.RightClickedItem;

return SidebarContext.IsItemRightClicked
&& item is not null
&& item.MenuOptions.IsLocationItem
&& !IsNonCopyableLocation(item);
}

private async Task CopyPathFallback(string path)
{
try
{
await FileOperationsHelpers.SetClipboard(new[] { path }, DataPackageOperation.Copy);
}
catch (Exception ex)
{
App.Logger.LogWarning(ex, "Failed to copy path to clipboard.");
}
}

private bool IsNonCopyableLocation(INavigationControlItem item)
{
if (string.IsNullOrEmpty(item.Path))
return true;

var normalizedPath = Constants.UserEnvironmentPaths.ShellPlaces.GetValueOrDefault(
item.Path.ToUpperInvariant(),
item.Path);

return item.Path.StartsWith("tag:", StringComparison.OrdinalIgnoreCase) ||
string.Equals(item.Path, "Home", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedPath, Constants.UserEnvironmentPaths.RecycleBinPath, StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedPath, Constants.UserEnvironmentPaths.NetworkFolderPath, StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedPath, Constants.UserEnvironmentPaths.MyComputerPath, StringComparison.OrdinalIgnoreCase);
}
}
}
9 changes: 9 additions & 0 deletions src/Files.App/Helpers/PathNormalization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ public static string Combine(string folder, string name)
if (string.IsNullOrEmpty(folder))
return name;

// Handle case where name is a rooted path (e.g., "E:\")
if (Path.IsPathRooted(name))
{
var root = Path.GetPathRoot(name);
if (!string.IsNullOrEmpty(root) && name.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) == root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
// Just use the drive letter
name = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, ':');
}

return folder.Contains('/', StringComparison.Ordinal) ? Path.Combine(folder, name).Replace("\\", "/", StringComparison.Ordinal) : Path.Combine(folder, name);
}
}
Expand Down
23 changes: 20 additions & 3 deletions src/Files.App/ViewModels/UserControls/SidebarViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -736,13 +736,25 @@ public async void HandleItemContextInvokedAsync(object sender, ItemContextInvoke

var itemContextMenuFlyout = new CommandBarFlyout()
{
Placement = FlyoutPlacementMode.Full
Placement = FlyoutPlacementMode.Right,
AlwaysExpanded = true
};

itemContextMenuFlyout.Opening += (sender, e) => App.LastOpenedFlyout = sender as CommandBarFlyout;

var menuItems = GetLocationItemMenuItems(item, itemContextMenuFlyout);
var (_, secondaryElements) = ContextFlyoutModelToElementHelper.GetAppBarItemsFromModel(menuItems);
var (primaryElements, secondaryElements) = ContextFlyoutModelToElementHelper.GetAppBarItemsFromModel(menuItems);

// Workaround for WinUI (#5508) - AppBarButtons don't auto-close CommandBarFlyout
var closeHandler = new RoutedEventHandler((s, e) => itemContextMenuFlyout.Hide());
primaryElements
.OfType<AppBarButton>()
.ForEach(button => button.Click += closeHandler);
primaryElements
.OfType<AppBarToggleButton>()
.ForEach(button => button.Click += closeHandler);

primaryElements.ForEach(itemContextMenuFlyout.PrimaryCommands.Add);

secondaryElements
.OfType<FrameworkElement>()
Expand Down Expand Up @@ -952,7 +964,7 @@ private List<ContextMenuFlyoutItemViewModel> GetLocationItemMenuItems(INavigatio

var isDriveItem = item is DriveItem;
var isDriveItemPinned = isDriveItem && ((DriveItem)item).IsPinned;

return new List<ContextMenuFlyoutItemViewModel>()
{
new ContextMenuFlyoutItemViewModel()
Expand Down Expand Up @@ -989,6 +1001,11 @@ private List<ContextMenuFlyoutItemViewModel> GetLocationItemMenuItems(INavigatio
{
IsVisible = UserSettingsService.GeneralSettingsService.ShowOpenInNewPane && Commands.OpenInNewPaneFromSidebar.IsExecutable
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.CopyItemFromSidebar)
{
IsPrimary = true,
IsVisible = Commands.CopyItemFromSidebar.IsExecutable
}.Build(),
new ContextMenuFlyoutItemViewModel()
{
Text = Strings.PinFolderToSidebar.GetLocalizedResource(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ widgetCardItem.DataContext is not WidgetCardItem item ||
// Create a new Flyout
var itemContextMenuFlyout = new CommandBarFlyout()
{
Placement = FlyoutPlacementMode.Right
Placement = FlyoutPlacementMode.Right,
AlwaysExpanded = true
};

// Hook events
Expand All @@ -78,7 +79,19 @@ widgetCardItem.DataContext is not WidgetCardItem item ||

// Get items for the flyout
var menuItems = GetItemMenuItems(item, QuickAccessService.IsItemPinned(item.Path), fileTagsCardItem is not null && fileTagsCardItem.IsFolder);
var (_, secondaryElements) = ContextFlyoutModelToElementHelper.GetAppBarItemsFromModel(menuItems);
var (primaryElements, secondaryElements) = ContextFlyoutModelToElementHelper.GetAppBarItemsFromModel(menuItems);

// Workaround for WinUI (#5508) - AppBarButtons don't auto-close CommandBarFlyout
var closeHandler = new RoutedEventHandler((s, e) => itemContextMenuFlyout.Hide());
primaryElements
.OfType<AppBarButton>()
.ForEach(button => button.Click += closeHandler);
primaryElements
.OfType<AppBarToggleButton>()
.ForEach(button => button.Click += closeHandler);

// Add menu items to the primary flyout
primaryElements.ForEach(itemContextMenuFlyout.PrimaryCommands.Add);

// Set max width of the flyout
secondaryElements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ public override List<ContextMenuFlyoutItemViewModel> GetItemMenuItems(WidgetCard
{
IsVisible = UserSettingsService.GeneralSettingsService.ShowOpenInNewPane && CommandManager.OpenInNewPaneFromHome.IsExecutable
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(CommandManager.CopyItemFromHome)
{
IsPrimary = true,
IsVisible = CommandManager.CopyItemFromHome.IsExecutable
}.Build(),
new()
{
Text = Strings.PinFolderToSidebar.GetLocalizedResource(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ public override List<ContextMenuFlyoutItemViewModel> GetItemMenuItems(WidgetCard
{
IsVisible = UserSettingsService.GeneralSettingsService.ShowOpenInNewPane && CommandManager.OpenInNewPaneFromHome.IsExecutable
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(CommandManager.CopyItemFromHome)
{
IsPrimary = true,
IsVisible = CommandManager.CopyItemFromHome.IsExecutable
}.Build(),
new()
{
Text = Strings.PinFolderToSidebar.GetLocalizedResource(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public override List<ContextMenuFlyoutItemViewModel> GetItemMenuItems(WidgetCard
{
IsVisible = UserSettingsService.GeneralSettingsService.ShowOpenInNewPane && CommandManager.OpenInNewPaneFromHome.IsExecutable
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(CommandManager.CopyItemFromHome)
{
IsPrimary = true,
IsVisible = CommandManager.CopyItemFromHome.IsExecutable
}.Build(),
new()
{
Text = Strings.PinFolderToSidebar.GetLocalizedResource(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ public override List<ContextMenuFlyoutItemViewModel> GetItemMenuItems(WidgetCard
{
return new List<ContextMenuFlyoutItemViewModel>()
{
new ContextMenuFlyoutItemViewModelBuilder(CommandManager.CopyItemFromHome)
{
IsPrimary = true,
IsVisible = CommandManager.CopyItemFromHome.IsExecutable
}.Build(),
new()
{
Text = Strings.OpenWith.GetLocalizedResource(),
Expand Down
Loading