diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs index e0d2c38262..f8e9478023 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs @@ -8,6 +8,7 @@ using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; namespace Microsoft.CmdPal.Core.ViewModels; @@ -16,6 +17,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa { public ExtensionObject Model => _commandItemModel; + private ExtensionObject? ExtendedAttributesProvider { get; set; } + private readonly ExtensionObject _commandItemModel = new(null); private CommandContextItemViewModel? _defaultCommandContextItemViewModel; @@ -65,6 +68,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa public bool ShouldBeVisible => !string.IsNullOrEmpty(Name); + public DataPackageView? DataPackage { get; private set; } + public List AllCommands { get @@ -157,6 +162,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa // will never be able to load Hotkeys & aliases UpdateProperty(nameof(IsInitialized)); + if (model is IExtendedAttributesProvider extendedAttributesProvider) + { + ExtendedAttributesProvider = new ExtensionObject(extendedAttributesProvider); + var properties = extendedAttributesProvider.GetProperties(); + UpdateDataPackage(properties); + } + Initialized |= InitializedState.Initialized; } @@ -379,6 +391,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa UpdateProperty(nameof(SecondaryCommandName)); UpdateProperty(nameof(HasMoreCommands)); + break; + case nameof(DataPackage): + UpdateDataPackage(ExtendedAttributesProvider?.Unsafe?.GetProperties()); break; } @@ -431,6 +446,16 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa UpdateProperty(nameof(Icon)); } + private void UpdateDataPackage(IDictionary? properties) + { + DataPackage = + properties?.TryGetValue(WellKnownExtensionAttributes.DataPackage, out var dataPackageView) == true && + dataPackageView is DataPackageView view + ? view + : null; + UpdateProperty(nameof(DataPackage)); + } + protected override void UnsafeCleanup() { base.UnsafeCleanup(); diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs index 76b5f786c8..4044800be6 100644 --- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs +++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs @@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Storage.Streams; namespace Microsoft.CmdPal.Core.ViewModels; @@ -57,7 +58,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData // because each call to GetProperties() is a cross process hop, and if you // marshal-by-value the property set, then you don't want to throw it away and // re-marshal it for every property. MAKE SURE YOU CACHE IT. - if (props?.TryGetValue("FontFamily", out var family) ?? false) + if (props?.TryGetValue(WellKnownExtensionAttributes.FontFamily, out var family) ?? false) { FontFamily = family as string; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index fc5e36d1e2..fcba26ade8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -18,7 +18,7 @@ using WyHash; namespace Microsoft.CmdPal.UI.ViewModels; -public sealed partial class TopLevelViewModel : ObservableObject, IListItem +public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider { private readonly SettingsModel _settings; private readonly ProviderSettings _providerSettings; @@ -232,6 +232,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { UpdateInitialIcon(); } + else if (e.PropertyName == nameof(CommandItem.DataPackage)) + { + DoOnUiThread(() => + { + OnPropertyChanged(nameof(CommandItem.DataPackage)); + }); + } } } @@ -394,4 +401,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}"; } + + public IDictionary GetProperties() + { + return new Dictionary + { + [WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage, + }; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index 859a74eb18..3508798078 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -439,9 +439,12 @@ DelayRenderer(request, item, format)); + } + catch (Exception) + { + // noop - skip any formats that fail + } + } + + WeakReferenceMessenger.Default.Send(new DragStartedMessage()); + } + catch (Exception ex) + { + WeakReferenceMessenger.Default.Send(new DragCompletedMessage()); + Logger.LogError("Failed to start dragging an item", ex); + } + } + + private static void DelayRenderer(DataProviderRequest request, ListItemViewModel item, string format) + { + var deferral = request.GetDeferral(); + try + { + item.DataPackage?.GetDataAsync(format) + .AsTask() + .ContinueWith(dataTask => + { + try + { + if (dataTask.IsCompletedSuccessfully) + { + request.SetData(dataTask.Result); + } + else if (dataTask.IsFaulted && dataTask.Exception is not null) + { + Logger.LogError($"Failed to get data for format '{format}' during drag-and-drop", dataTask.Exception); + } + } + finally + { + deferral.Complete(); + } + }); + } + catch (Exception ex) + { + Logger.LogError($"Failed to set data for format '{format}' during drag-and-drop", ex); + deferral.Complete(); + } + } + + private void Items_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args) + { + WeakReferenceMessenger.Default.Send(new DragCompletedMessage()); + } + /// /// Code stealed from /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index d9acdb48d9..54e9b8a216 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -52,6 +52,8 @@ public sealed partial class MainWindow : WindowEx, IRecipient, IRecipient, IRecipient, + IRecipient, + IRecipient, IDisposable { private const int DefaultWidth = 800; @@ -79,6 +81,8 @@ public sealed partial class MainWindow : WindowEx, private WindowPosition _currentWindowPosition = new(); + private bool _preventHideWhenDeactivated; + private MainWindowViewModel ViewModel { get; } public MainWindow() @@ -119,6 +123,8 @@ public sealed partial class MainWindow : WindowEx, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); // Hide our titlebar. // We need to both ExtendsContentIntoTitleBar, then set the height to Collapsed @@ -751,6 +757,12 @@ public sealed partial class MainWindow : WindowEx, return; } + // We're doing something that requires us to lose focus, but we don't want to hide the window + if (_preventHideWhenDeactivated) + { + return; + } + // This will DWM cloak our window: HideWindow(); @@ -1027,4 +1039,44 @@ public sealed partial class MainWindow : WindowEx, _windowThemeSynchronizer.Dispose(); DisposeAcrylic(); } + + public void Receive(DragStartedMessage message) + { + _preventHideWhenDeactivated = true; + } + + public void Receive(DragCompletedMessage message) + { + _preventHideWhenDeactivated = false; + Task.Delay(200).ContinueWith(_ => + { + DispatcherQueue.TryEnqueue(StealForeground); + }); + } + + private unsafe void StealForeground() + { + var foregroundWindow = PInvoke.GetForegroundWindow(); + if (foregroundWindow == _hwnd) + { + return; + } + + // This is bad, evil, and I'll have to forgo today's dinner dessert to punish myself + // for writing this. But there's no way to make this work without it. + // If the window is not reactivated, the UX breaks down: a deactivated window has to + // be activated and then deactivated again to hide. + var currentThreadId = PInvoke.GetCurrentThreadId(); + var foregroundThreadId = PInvoke.GetWindowThreadProcessId(foregroundWindow, null); + if (foregroundThreadId != currentThreadId) + { + PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, true); + PInvoke.SetForegroundWindow(_hwnd); + PInvoke.AttachThreadInput(currentThreadId, foregroundThreadId, false); + } + else + { + PInvoke.SetForegroundWindow(_hwnd); + } + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragCompletedMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragCompletedMessage.cs new file mode 100644 index 0000000000..c2bd5300ed --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragCompletedMessage.cs @@ -0,0 +1,7 @@ +// 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. + +namespace Microsoft.CmdPal.UI.Messages; + +public record DragCompletedMessage; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragStartedMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragStartedMessage.cs new file mode 100644 index 0000000000..84b9915fc6 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Messages/DragStartedMessage.cs @@ -0,0 +1,7 @@ +// 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. + +namespace Microsoft.CmdPal.UI.Messages; + +public record DragStartedMessage; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt index fc5a608199..513db65b1a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt @@ -63,4 +63,7 @@ CreateWindowEx WNDCLASSEXW RegisterClassEx GetStockObject -GetModuleHandle \ No newline at end of file +GetModuleHandle + +GetWindowThreadProcessId +AttachThreadInput \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs index 865d8f6b91..bd1ad3d1c1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Pages/ClipboardListItem.cs @@ -12,6 +12,8 @@ using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers; using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers.Analyzers; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.ApplicationModel.DataTransfer; +using WinRT; namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models; @@ -62,6 +64,8 @@ internal sealed partial class ClipboardListItem : ListItem RequestedShortcut = KeyChords.DeleteEntry, }; + DataPackageView = _item.Item.Content; + if (item.IsImage) { Title = "Image"; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs index 7bb1fb4733..51a30ddc86 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Data/IndexerListItem.cs @@ -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()) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs index 5e044247ba..9e2302c630 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -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; } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Helpers/DataPackageHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Helpers/DataPackageHelper.cs new file mode 100644 index 0000000000..65d18a0e2a --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Helpers/DataPackageHelper.cs @@ -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 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; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs index 79a47543b6..9bd575e87a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/ExploreListItem.cs @@ -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 context = []; if (indexerItem.IsDirectory()) { diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/SampleDataTransferPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/SampleDataTransferPage.cs new file mode 100644 index 0000000000..842f128842 --- /dev/null +++ b/src/modules/cmdpal/ext/SamplePagesExtension/SampleDataTransferPage.cs @@ -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; + } +} diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs index 1c8ae56ee5..3dd67086c8 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs @@ -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()) diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs index cddb678fa3..46c32765a9 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/CommandItem.cs @@ -2,14 +2,23 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation.Collections; +using WinRT; + namespace Microsoft.CommandPalette.Extensions.Toolkit; -public partial class CommandItem : BaseObservable, ICommandItem +public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttributesProvider { + private readonly PropertySet _extendedAttributes = new(); + private ICommand? _command; private WeakEventListener? _commandListener; private string _title = string.Empty; + private DataPackage? _dataPackage; + private DataPackageView? _dataPackageView; + public virtual IIconInfo? Icon { get => field; @@ -91,6 +100,32 @@ public partial class CommandItem : BaseObservable, ICommandItem = []; + public DataPackage? DataPackage + { + get => _dataPackage; + set + { + _dataPackage = value; + _dataPackageView = null; + _extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()?.GetView()!; + OnPropertyChanged(nameof(DataPackage)); + OnPropertyChanged(nameof(DataPackageView)); + } + } + + public DataPackageView? DataPackageView + { + get => _dataPackageView; + set + { + _dataPackage = null; + _dataPackageView = value; + _extendedAttributes[WellKnownExtensionAttributes.DataPackage] = value?.AsAgile().Get()!; + OnPropertyChanged(nameof(DataPackage)); + OnPropertyChanged(nameof(DataPackageView)); + } + } + public CommandItem() : this(new NoOpCommand()) { @@ -132,4 +167,9 @@ public partial class CommandItem : BaseObservable, ICommandItem Title = title; Subtitle = subtitle; } + + public IDictionary GetProperties() + { + return _extendedAttributes; + } } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs index 7e12d38d0c..3fcc01b96d 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FontIconData.cs @@ -27,6 +27,6 @@ public partial class FontIconData : IconData, IExtendedAttributesProvider public IDictionary? GetProperties() => new ValueSet() { - { "FontFamily", FontFamily }, + { WellKnownExtensionAttributes.FontFamily, FontFamily }, }; } diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/WellKnownExtensionAttributes.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/WellKnownExtensionAttributes.cs new file mode 100644 index 0000000000..508cda72c1 --- /dev/null +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/WellKnownExtensionAttributes.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.CommandPalette.Extensions.Toolkit; + +public static class WellKnownExtensionAttributes +{ + public const string DataPackage = "Microsoft.CommandPalette.DataPackage"; + + public const string FontFamily = "FontFamily"; +}