Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Windows.ApplicationModel.DataTransfer;
using Windows.ApplicationModel.DataTransfer.DragDrop;
using Windows.ApplicationModel.DataTransfer.DragDrop.Core;
using Windows.Foundation;
using Windows.Storage.Streams;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.System.Com;
using Windows.Win32.System.Ole;
using Windows.Win32.System.SystemServices;
Expand All @@ -20,6 +24,7 @@
using Uno.UI.Hosting;
using Uno.UI.NativeElementHosting;
using IDataObject = Windows.Win32.System.Com.IDataObject;
using Microsoft.UI.Input;

namespace Uno.UI.Runtime.Skia.Win32;

Expand Down Expand Up @@ -149,8 +154,11 @@ unsafe HRESULT IDropTarget.Interface.DragEnter(IDataObject* dataObject, MODIFIER
return medium.u.hGlobal;
});

// Create DragUI for visual feedback during drag operation
var dragUI = CreateDragUIForExternalDrag(dataObject, formatEtcList);

// DROPEFFECT and DataPackageOperation have the same binary representation
var info = new CoreDragInfo(src, package.GetView(), (DataPackageOperation)(*pdwEffect));
var info = new CoreDragInfo(src, package.GetView(), (DataPackageOperation)(*pdwEffect), dragUI);
_coreDragDropManager.DragStarted(info);

*pdwEffect = (DROPEFFECT)_manager.ProcessMoved(src);
Expand Down Expand Up @@ -199,6 +207,203 @@ unsafe HRESULT IDropTarget.Interface.Drop(IDataObject* dataObject, MODIFIERKEYS_

public void StartNativeDrag(CoreDragInfo info, Action<DataPackageOperation> action) => throw new System.NotImplementedException();

private static unsafe DragUI? CreateDragUIForExternalDrag(IDataObject* dataObject, FORMATETC[] formatEtcList)
{
var dragUI = new DragUI();

// Check if we have a DIB (Device Independent Bitmap) format
var dibFormatIndex = Array.FindIndex(formatEtcList, f => f.cfFormat == (int)CLIPBOARD_FORMAT.CF_DIB);
if (dibFormatIndex >= 0)
{
var dibFormat = formatEtcList[dibFormatIndex];
// Try to get the DIB data directly
var hResult = dataObject->GetData(dibFormat, out STGMEDIUM dibMedium);
if (hResult.Succeeded && dibMedium.tymed == TYMED.TYMED_HGLOBAL && dibMedium.u.hGlobal != IntPtr.Zero)
{
try
{
var unoImage = ConvertDibToUnoBitmapImage(dibMedium.u.hGlobal);
if (unoImage is not null)
{
dragUI.SetContentFromExternalBitmapImage(unoImage);
return dragUI;
}
}
catch (Exception ex)
{
// If we can't load the image, continue without visual feedback
var logger = typeof(Win32DragDropExtension).Log();
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug($"Failed to load image thumbnail for drag operation: {ex.Message}");
}
}
finally
{
PInvoke.ReleaseStgMedium(&dibMedium);
}
}
}

// Check if we have file drop format
var hdropFormatIndex = Array.FindIndex(formatEtcList, f => f.cfFormat == (int)CLIPBOARD_FORMAT.CF_HDROP);
if (hdropFormatIndex >= 0)
{
var hdropFormat = formatEtcList[hdropFormatIndex];
// Try to get the HDROP data directly
var hResult = dataObject->GetData(hdropFormat, out STGMEDIUM hdropMedium);
if (hResult.Succeeded && hdropMedium.u.hGlobal != IntPtr.Zero)
{
try
{
var filePaths = ExtractFilePathsFromHDrop(hdropMedium.u.hGlobal);
var imageFile = filePaths.FirstOrDefault(f => IsImageFile(f));
if (imageFile is not null)
{
try
{
var unoImage = LoadImageFromFile(imageFile);
if (unoImage is not null)
{
dragUI.SetContentFromExternalBitmapImage(unoImage);
return dragUI;
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException or UriFormatException)
{
// If we can't load the image, continue without visual feedback
var logger = typeof(Win32DragDropExtension).Log();
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug($"Failed to load image thumbnail for drag operation: {ex.Message}");
}
}
}
}
finally
{
PInvoke.ReleaseStgMedium(&hdropMedium);
}
}
}

return dragUI;
}

private static bool IsImageFile(string filePath)
{
// Common image formats
// Note: Additional formats can be added here as needed
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return extension is ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".ico";
}

private static unsafe List<string> ExtractFilePathsFromHDrop(HGLOBAL handle)
{
var filePaths = new List<string>();

using var lockDisposable = Win32Helper.GlobalLock(handle, out var firstByte);
if (lockDisposable is null)
{
return filePaths;
}

var hDrop = new Windows.Win32.UI.Shell.HDROP((IntPtr)firstByte);
var filesDropped = PInvoke.DragQueryFile(hDrop, 0xFFFFFFFF, new PWSTR(), 0);

for (uint i = 0; i < filesDropped; i++)
{
var charLength = PInvoke.DragQueryFile(hDrop, i, new PWSTR(), 0);
if (charLength == 0)
{
continue;
}
charLength++; // + 1 for \0

var buffer = Marshal.AllocHGlobal((IntPtr)(charLength * sizeof(char)));
try
{
var charsWritten = PInvoke.DragQueryFile(hDrop, i, new PWSTR((char*)buffer), charLength);
if (charsWritten > 0)
{
var path = Marshal.PtrToStringUni(buffer);
if (!string.IsNullOrEmpty(path))
{
filePaths.Add(path);
}
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}

return filePaths;
}

private static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? LoadImageFromFile(string filePath)
{
try
{
// Validate file path to prevent potential security issues
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}

// Load image from file
using var fileStream = File.OpenRead(filePath);
var unoBitmap = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage();
unoBitmap.SetSource(fileStream.AsRandomAccessStream());

return unoBitmap;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException or UriFormatException)
{
// Failed to load image - file might not exist, no access, unsupported format, or invalid path
return null;
}
}

private static unsafe Microsoft.UI.Xaml.Media.Imaging.BitmapImage? ConvertDibToUnoBitmapImage(HGLOBAL handle)
{
try
{
using var lockDisposable = Win32Helper.GlobalLock(handle, out var dib);
if (lockDisposable is null)
{
return null;
}

var memSize = (uint)PInvoke.GlobalSize(handle);
if (memSize <= Marshal.SizeOf<BITMAPINFOHEADER>())
{
return null;
}

// Convert DIB to a stream that can be used by BitmapImage
// Pre-allocate buffer for typical thumbnail size to avoid reallocations
using var memoryStream = new MemoryStream(capacity: 8192);

// Copy the DIB data to the memory stream
var dibBytes = new Span<byte>(dib, (int)memSize);
memoryStream.Write(dibBytes);
memoryStream.Position = 0;

// Create Uno BitmapImage from the stream
var unoBitmap = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage();
unoBitmap.SetSource(memoryStream.AsRandomAccessStream());

return unoBitmap;
}
catch (Exception ex) when (ex is IOException or NotSupportedException or InvalidOperationException)
{
// Failed to convert bitmap - encoding or stream operations failed
return null;
}
}

private readonly struct DragEventSource(Point point, MODIFIERKEYS_FLAGS modifierFlags) : IDragEventSource
{
private static long _nextFrameId;
Expand Down
116 changes: 115 additions & 1 deletion src/Uno.UI.Runtime.Skia.Wpf/Extensions/WpfDragDropExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Uno.Foundation.Logging;
using Uno.UI.Hosting;
using Uno.UI.Runtime.Skia.Wpf.Hosting;
using Microsoft.UI.Input;

namespace Uno.UI.Runtime.Skia.Wpf
{
Expand Down Expand Up @@ -53,7 +54,8 @@ private void OnHostDragEnter(object sender, DragEventArgs e)
var src = new DragEventSource(_fakePointerId, e);
var data = ToDataPackage(e.Data);
var allowedOperations = ToDataPackageOperation(e.AllowedEffects);
var info = new CoreDragInfo(src, data.GetView(), allowedOperations);
var dragUI = CreateDragUIForExternalDrag(e.Data);
var info = new CoreDragInfo(src, data.GetView(), allowedOperations, dragUI);

_coreDragDropManager.DragStarted(info);

Expand Down Expand Up @@ -173,6 +175,118 @@ private static DataPackage ToDataPackage(IDataObject src)
return dst;
}

private static DragUI? CreateDragUIForExternalDrag(IDataObject src)
{
var dragUI = new DragUI(PointerDeviceType.Mouse);

// Check if we're dragging an image file that can be shown as a thumbnail
if (src.GetData(DataFormats.Bitmap) is BitmapSource bitmap)
{
// Convert WPF BitmapSource to Uno BitmapImage for DragUI
var unoImage = ConvertBitmapSourceToUnoBitmapImage(bitmap);
if (unoImage is not null)
{
dragUI.SetContentFromExternalBitmapImage(unoImage);
return dragUI;
}
}

// If we have file paths, try to load the first image file as a thumbnail
if (src.GetData(DataFormats.FileDrop) is string[] files && files.Length > 0)
{
var imageFile = files.FirstOrDefault(f => IsImageFile(f));
if (imageFile is not null)
{
try
{
var unoImage = LoadImageFromFile(imageFile);
if (unoImage is not null)
{
dragUI.SetContentFromExternalBitmapImage(unoImage);
return dragUI;
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException or UriFormatException)
{
// If we can't load the image (file not found, no access, unsupported format, or invalid path),
// continue without visual feedback
var logger = typeof(WpfDragDropExtension).Log();
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug($"Failed to load image thumbnail for drag operation: {ex.Message}");
}
}
}
}

return dragUI;
}

private static bool IsImageFile(string filePath)
{
// Common image formats supported by WPF BitmapImage
// Note: Additional formats can be added here as needed
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return extension is ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".tiff" or ".ico";
}

private static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? LoadImageFromFile(string filePath)
{
try
{
// Validate file path to prevent potential security issues
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
{
return null;
}

// Load the WPF bitmap
var wpfBitmap = new System.Windows.Media.Imaging.BitmapImage();
wpfBitmap.BeginInit();
wpfBitmap.CacheOption = BitmapCacheOption.OnLoad;
wpfBitmap.CreateOptions = BitmapCreateOptions.None;
wpfBitmap.UriSource = new Uri(filePath, UriKind.Absolute);
wpfBitmap.DecodePixelWidth = 96; // Limit thumbnail size
wpfBitmap.EndInit();
wpfBitmap.Freeze();

// Convert to Uno BitmapImage
return ConvertBitmapSourceToUnoBitmapImage(wpfBitmap);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or NotSupportedException or UriFormatException)
{
// Failed to load image - file might not exist, no access, unsupported format, or invalid path
return null;
}
}

private static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? ConvertBitmapSourceToUnoBitmapImage(BitmapSource wpfBitmap)
{
try
{
// Encode the WPF bitmap to a stream
// Note: The using statement disposes the MemoryStream after SetSource() completes.
// This is safe because SetSource() reads and copies the stream data synchronously.
// Pre-allocate buffer for typical thumbnail size to avoid reallocations
using var memoryStream = new MemoryStream(capacity: 8192);
var encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(wpfBitmap));
encoder.Save(memoryStream);
memoryStream.Position = 0;

// Create Uno BitmapImage from the stream
var unoBitmap = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage();
unoBitmap.SetSource(memoryStream.AsRandomAccessStream());

return unoBitmap;
}
catch (Exception ex) when (ex is IOException or NotSupportedException or InvalidOperationException)
{
// Failed to convert bitmap - encoding or stream operations failed
return null;
}
}

private static async Task<DataObject> ToDataObject(DataPackageView src, CancellationToken ct)
{
var dst = new DataObject();
Expand Down
Loading
Loading