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:
Jiří Polášek
2025-12-11 15:05:48 +01:00
committed by GitHub
parent 4de4d5f310
commit 73786cd2be
19 changed files with 611 additions and 11 deletions

View File

@@ -0,0 +1,254 @@
// 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.Globalization;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage.Streams;
namespace SamplePagesExtension;
internal sealed partial class SampleDataTransferPage : ListPage
{
private readonly IListItem[] _items;
public SampleDataTransferPage()
{
var dataPackageWithText = CreateDataPackageWithText();
var dataPackageWithDelayedText = CreateDataPackageWithDelayedText();
var dataPackageWithImage = CreateDataPackageWithImage();
_items =
[
new ListItem(new NoOpCommand())
{
Title = "Draggable item with a plain text",
Subtitle = "A sample page demonstrating how to drag and drop data",
DataPackage = dataPackageWithText,
},
new ListItem(new NoOpCommand())
{
Title = "Draggable item with a lazily rendered plain text",
Subtitle = "A sample page demonstrating how to drag and drop data with delayed rendering",
DataPackage = dataPackageWithDelayedText,
},
new ListItem(new NoOpCommand())
{
Title = "Draggable item with an image",
Subtitle = "This item has an image - package contains both file and a bitmap",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = dataPackageWithImage,
},
new ListItem(new SampleDataTransferOnGridPage())
{
Title = "Drag & drop grid",
Subtitle = "A sample page demonstrating a grid list of items",
Icon = new IconInfo("\uF0E2"),
}
];
}
private static DataPackage CreateDataPackageWithText()
{
var dataPackageWithText = new DataPackage
{
Properties =
{
Title = "Item with data package with text",
Description = "This item has associated text with it",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithText.SetText("Text data in the Data Package");
return dataPackageWithText;
}
private static DataPackage CreateDataPackageWithDelayedText()
{
var dataPackageWithDelayedText = new DataPackage
{
Properties =
{
Title = "Item with delayed render data in the data package",
Description = "This items has an item associated with it that is evaluated when requested for the first time",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithDelayedText.SetDataProvider(StandardDataFormats.Text, request =>
{
var d = request.GetDeferral();
try
{
request.SetData(DateTime.Now.ToString("G", CultureInfo.CurrentCulture));
}
finally
{
d.Complete();
}
});
return dataPackageWithDelayedText;
}
private static DataPackage CreateDataPackageWithImage()
{
var dataPackageWithImage = new DataPackage
{
Properties =
{
Title = "Item with delayed render image in the data package",
Description = "This items has an image associated with it that is evaluated when requested for the first time",
},
RequestedOperation = DataPackageOperation.Copy,
};
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async void (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
request.SetData(streamRef);
}
finally
{
deferral.Complete();
}
});
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async void (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Assets/Images/Swirls.png"));
var items = new[] { file };
request.SetData(items);
}
finally
{
deferral.Complete();
}
});
return dataPackageWithImage;
}
public override IListItem[] GetItems() => _items;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Samples")]
internal sealed partial class SampleDataTransferOnGridPage : ListPage
{
public SampleDataTransferOnGridPage()
{
GridProperties = new GalleryGridLayout
{
ShowTitle = true,
ShowSubtitle = true,
};
}
public override IListItem[] GetItems()
{
return [
new ListItem(new NoOpCommand())
{
Title = "Red Rectangle",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Swirls",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Windows Digital",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Red Rectangle",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/RedRectangle.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/RedRectangle.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Space",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Space.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Space.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Swirls",
Subtitle = "Drop me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Swirls.png"),
},
new ListItem(new NoOpCommand())
{
Title = "Windows Digital",
Subtitle = "Drag me",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
DataPackage = CreateDataPackageForImage("Assets/Images/Win-Digital.png"),
},
];
}
private static DataPackage CreateDataPackageForImage(string relativePath)
{
var dataPackageWithImage = new DataPackage
{
Properties =
{
Title = "Image",
Description = "This item has an image associated with it.",
},
RequestedOperation = DataPackageOperation.Copy,
};
var imageUri = new Uri($"ms-appx:///{relativePath}");
dataPackageWithImage.SetDataProvider(StandardDataFormats.Bitmap, async (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
var stream = await file.OpenAsync(Windows.Storage.FileAccessMode.Read);
var streamRef = RandomAccessStreamReference.CreateFromStream(stream);
request.SetData(streamRef);
}
finally
{
deferral.Complete();
}
});
dataPackageWithImage.SetDataProvider(StandardDataFormats.StorageItems, async (request) =>
{
var deferral = request.GetDeferral();
try
{
var file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(imageUri);
var items = new[] { file };
request.SetData(items);
}
finally
{
deferral.Complete();
}
});
return dataPackageWithImage;
}
}

View File

@@ -106,6 +106,13 @@ public partial class SamplesListPage : ListPage
Subtitle = "A demo of the settings helpers",
},
// Data package samples
new ListItem(new SampleDataTransferPage())
{
Title = "Clipboard and Drag-and-Drop Demo",
Subtitle = "Demonstrates clipboard integration and drag-and-drop functionality",
},
// Evil edge cases
// Anything weird that might break the palette - put that in here.
new ListItem(new EvilSamplesPage())