mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-05 18:57:19 +02:00
CmdPal: Add drag & drop support (#44165)
## Summary of the Pull Request This PR adds basic drag-and-drop support for items in list and grid views. It introduces two new properties on `ListItem`, backed by `IExtendedAttributesProvider`: `DataPackage` and `DataPackageView`. These properties are mutually exclusive. `DataPackage` serves as a convenience property allowing the item to retain the underlying object without risk of losing it. Across the extension boundary, only the immutable `DataPackageView` snapshot is transferred. When `DataPackage` is set, `DataPackageView` is derived from it. This PR includes initial concrete drag-and-drop implementations for: - File Indexer - Clipboard History **Todo / Missing pieces** - [x] Extend `DataPackage` support to top-level command items, enabling scenarios such as index fallback ~ - [x] Provide automatic drag-and-drop for unconfigured list items (e.g., copying title and subtitle as text) - [x] Keep CmdPal open - [ ] ~Clipboard commands (since we have the DataPackage...)~ - [ ] ~Improve logging~ ## Pictures? Moving ones! https://github.com/user-attachments/assets/13eb9a71-e760-43ea-8c2d-cd41cf377905 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #38289 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Core.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Pages;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation.Metadata;
|
||||
using FileAttributes = System.IO.FileAttributes;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
|
||||
@@ -36,6 +39,8 @@ internal sealed partial class IndexerListItem : ListItem
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
|
||||
|
||||
var commands = FileCommands(indexerItem.FullPath, browseByDefault);
|
||||
if (commands.Any())
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
@@ -42,6 +43,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -53,6 +55,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = string.Empty;
|
||||
Icon = null;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -67,6 +70,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Subtitle = item.FileName;
|
||||
Title = item.FullPath;
|
||||
Icon = listItemForUs.Icon;
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(listItemForUs, item.FullPath);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -92,13 +96,15 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
_searchEngine.Query(query, _queryCookie);
|
||||
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
|
||||
|
||||
if (results.Count == 0 || ((results[0] as IndexerListItem) is null))
|
||||
if (results.Count == 0 || (results[0] is not IndexerListItem indexerListItem))
|
||||
{
|
||||
// Exit 2: We searched for the file, and found nothing. Oh well.
|
||||
// Hide ourselves.
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
Command = new NoOpCommand();
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,11 +112,12 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
{
|
||||
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
|
||||
// Return it.
|
||||
Title = results[0].Title;
|
||||
Subtitle = results[0].Subtitle;
|
||||
Icon = results[0].Icon;
|
||||
Command = results[0].Command;
|
||||
MoreCommands = results[0].MoreCommands;
|
||||
Title = indexerListItem.Title;
|
||||
Subtitle = indexerListItem.Subtitle;
|
||||
Icon = indexerListItem.Icon;
|
||||
Command = indexerListItem.Command;
|
||||
MoreCommands = indexerListItem.MoreCommands;
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(indexerListItem, indexerListItem.FilePath);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -121,6 +128,8 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
|
||||
Icon = Icons.FileExplorerIcon;
|
||||
Command = indexerPage;
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -131,6 +140,7 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System
|
||||
Icon = null;
|
||||
Command = new NoOpCommand();
|
||||
MoreCommands = null;
|
||||
DataPackage = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
using File = System.IO.File;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
|
||||
internal static class DataPackageHelper
|
||||
{
|
||||
public static DataPackage CreateDataPackageForPath(ICommandItem listItem, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataPackage = new DataPackage();
|
||||
dataPackage.SetText(path);
|
||||
_ = dataPackage.TrySetStorageItemsAsync(path);
|
||||
dataPackage.Properties.Title = listItem.Title;
|
||||
dataPackage.Properties.Description = listItem.Subtitle;
|
||||
dataPackage.RequestedOperation = DataPackageOperation.Copy;
|
||||
return dataPackage;
|
||||
}
|
||||
|
||||
public static async Task<bool> TrySetStorageItemsAsync(this DataPackage dataPackage, string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var file = await StorageFile.GetFileFromPathAsync(filePath);
|
||||
dataPackage.SetStorageItems([file]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Directory.Exists(filePath))
|
||||
{
|
||||
var folder = await StorageFolder.GetFolderFromPathAsync(filePath);
|
||||
dataPackage.SetStorageItems([folder]);
|
||||
return true;
|
||||
}
|
||||
|
||||
// nothing there
|
||||
return false;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
// Access denied – skip or report, but don't crash
|
||||
return false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Core.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
@@ -28,6 +29,9 @@ internal sealed partial class ExploreListItem : ListItem
|
||||
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
|
||||
DataPackage = DataPackageHelper.CreateDataPackageForPath(this, FilePath);
|
||||
|
||||
List<CommandContextItem> context = [];
|
||||
if (indexerItem.IsDirectory())
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user