Skip to content

Commit

Permalink
[PTRun] Drag and drop files (#22409)
Browse files Browse the repository at this point in the history
* [PTRun] Support drag&drop to other application for files in result list

* [PTRun] use file/folder thumbnail as drag image

* (fix spellcheck)

* [PTRun] use _mouseDownResultViewModel.Image to generate the drag image

* fix spelling + refactoring
  • Loading branch information
daniel-richter authored Dec 9, 2022
1 parent bb92b03 commit 08d569c
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
try
{
Clipboard.SetText(record.FullPath);
Clipboard.SetText(record.Path);
return true;
}
catch (Exception e)
Expand Down Expand Up @@ -75,18 +75,18 @@ public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
if (record.Type == ResultType.File)
{
Helper.OpenInConsole(_fileSystem.Path.GetDirectoryName(record.FullPath));
Helper.OpenInConsole(_fileSystem.Path.GetDirectoryName(record.Path));
}
else
{
Helper.OpenInConsole(record.FullPath);
Helper.OpenInConsole(record.Path);
}

return true;
}
catch (Exception e)
{
Log.Exception($"Failed to open {record.FullPath} in console, {e.Message}", e, GetType());
Log.Exception($"Failed to open {record.Path} in console, {e.Message}", e, GetType());

return false;
}
Expand All @@ -109,9 +109,9 @@ private ContextMenuResult CreateOpenContainingFolderResult(SearchResult record)
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
if (!Helper.OpenInShell("explorer.exe", $"/select,\"{record.FullPath}\""))
if (!Helper.OpenInShell("explorer.exe", $"/select,\"{record.Path}\""))
{
var message = $"{Properties.Resources.Microsoft_plugin_folder_file_open_failed} {record.FullPath}";
var message = $"{Properties.Resources.Microsoft_plugin_folder_file_open_failed} {record.Path}";
_context.API.ShowMsg(message);
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace Microsoft.Plugin.Folder
{
public class SearchResult
using Wox.Plugin.Interfaces;

public class SearchResult : IFileDropResult
{
public string FullPath { get; set; }
public string Path { get; set; }

public ResultType Type { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public Wox.Plugin.Result Create(IPublicAPI contextApi)
IcoPath = Search,
Score = 500,
Action = c => _shellAction.ExecuteSanitized(Search, contextApi),
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = Search },
ContextData = new SearchResult { Type = ResultType.Folder, Path = Search },
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public Wox.Plugin.Result Create(IPublicAPI contextApi)
SubTitle = string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_folder_result_subtitle, Path),
ToolTipData = new ToolTipData(Title, string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_folder_result_subtitle, Path)),
QueryTextDisplay = Path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = Path },
ContextData = new SearchResult { Type = ResultType.Folder, Path = Path },
Action = c => _shellAction.Execute(Path, contextApi),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public Wox.Plugin.Result Create(IPublicAPI contextApi)
ToolTipData = new ToolTipData(Title, string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_file_result_subtitle, FilePath)),
IcoPath = FilePath,
Action = c => ShellAction.Execute(FilePath, contextApi),
ContextData = new SearchResult { Type = ResultType.File, FullPath = FilePath },
ContextData = new SearchResult { Type = ResultType.File, Path = FilePath },
};
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public Wox.Plugin.Result Create(IPublicAPI contextApi)
SubTitle = string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_folder_result_subtitle, Subtitle),
ToolTipData = new ToolTipData(Title, string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_folder_result_subtitle, Subtitle)),
QueryTextDisplay = Path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = Path },
ContextData = new SearchResult { Type = ResultType.Folder, Path = Path },
Action = c => ShellAction.Execute(Path, contextApi),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Result Create(IPublicAPI contextApi)
// Using CurrentCulture since this is user facing
SubTitle = string.Format(CultureInfo.CurrentCulture, Properties.Resources.wox_plugin_folder_select_folder_result_subtitle, Subtitle),
QueryTextDisplay = Path,
ContextData = new SearchResult { Type = ResultType.Folder, FullPath = Path },
ContextData = new SearchResult { Type = ResultType.Folder, Path = Path },
Action = c => _shellAction.Execute(Path, contextApi),
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Microsoft.Plugin.Indexer.SearchHelper
{
public class SearchResult
using Wox.Plugin.Interfaces;

public class SearchResult : IFileDropResult
{
// Contains the Path of the file or folder
public string Path { get; set; }
Expand Down
99 changes: 99 additions & 0 deletions src/modules/launcher/PowerLauncher/Helper/DragDataObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// 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 PowerLauncher.Helper
{
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using DrawingImaging = System.Drawing.Imaging;
using MediaImaging = System.Windows.Media.Imaging;

// based on: https://stackoverflow.com/questions/61041282/showing-image-thumbnail-with-mouse-cursor-while-dragging/61148788#61148788
public static class DragDataObject
{
private static readonly Guid DataObject = new Guid("b8c0bd9f-ed24-455c-83e6-d5390c4fe8c4");

public static IDataObject FromFile(string filePath)
{
Marshal.ThrowExceptionForHR(SHCreateItemFromParsingName(filePath, null, typeof(IShellItem).GUID, out IShellItem item));
Marshal.ThrowExceptionForHR(item.BindToHandler(null, DataObject, typeof(IDataObject).GUID, out object dataObject));
return (IDataObject)dataObject;
}

public static void SetDragImage(this IDataObject dataObject, IntPtr hBitmap, int width, int height)
{
if (dataObject == null)
{
throw new ArgumentNullException(nameof(dataObject));
}

IDragSourceHelper dragDropHelper = (IDragSourceHelper)new DragDropHelper();
ShDragImage dragImage = new ShDragImage
{
HBmpDragImage = hBitmap,
SizeDragImage = new Size(width, height),
};
Marshal.ThrowExceptionForHR(dragDropHelper.InitializeFromBitmap(ref dragImage, dataObject));
}

[DllImport("shell32", CharSet = CharSet.Unicode)]
private static extern int SHCreateItemFromParsingName(string path, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv);

[Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
private interface IShellItem
{
[PreserveSig]
int BindToHandler(IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid bhid, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);

// more methods available, but we don't need them
}

[ComImport]
[Guid("4657278a-411b-11d2-839a-00c04fd918d0")] // CLSID_DragDropHelper
private class DragDropHelper
{
}

// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/ns-shobjidl_core-shdragimage
[StructLayout(LayoutKind.Sequential)]
private struct ShDragImage
{
public Size SizeDragImage;
public Point PtOffset;
public IntPtr HBmpDragImage;
public int CrColorKey;
}

// https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nn-shobjidl_core-idragsourcehelper
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("DE5BF786-477A-11D2-839D-00C04FD918D0")]
private interface IDragSourceHelper
{
[PreserveSig]
int InitializeFromBitmap(ref ShDragImage pShDrawImage, IDataObject pDataObject);

// more methods available, but we don't need them
}

// https://stackoverflow.com/a/2897325
public static Bitmap BitmapSourceToBitmap(MediaImaging.BitmapSource source)
{
if (source == null)
{
return null;
}

Bitmap bitmap = new Bitmap(source.PixelWidth, source.PixelHeight, DrawingImaging.PixelFormat.Format32bppArgb);
DrawingImaging.BitmapData bitmapData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), DrawingImaging.ImageLockMode.WriteOnly, DrawingImaging.PixelFormat.Format32bppArgb);

source.CopyPixels(System.Windows.Int32Rect.Empty, bitmapData.Scan0, bitmapData.Height * bitmapData.Stride, bitmapData.Stride);
bitmap.UnlockBits(bitmapData);

return bitmap;
}
}
}
51 changes: 51 additions & 0 deletions src/modules/launcher/PowerLauncher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
Expand All @@ -12,6 +13,7 @@
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
using Common.UI;
using interop;
using Microsoft.PowerLauncher.Telemetry;
Expand All @@ -21,7 +23,10 @@
using PowerLauncher.Telemetry.Events;
using PowerLauncher.ViewModel;
using Wox.Infrastructure.UserSettings;
using Wox.Plugin;
using Wox.Plugin.Interfaces;
using CancellationToken = System.Threading.CancellationToken;
using Image = Wox.Infrastructure.Image;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using Log = Wox.Plugin.Logger.Log;
using Screen = System.Windows.Forms.Screen;
Expand All @@ -40,6 +45,8 @@ public partial class MainWindow : IDisposable
private bool _coldStateHotkeyPressed;
private bool _disposedValue;
private IDisposable _reactiveSubscription;
private Point _mouseDownPosition;
private ResultViewModel _mouseDownResultViewModel;

public MainWindow(PowerToysRunSettings settings, MainViewModel mainVM, CancellationToken nativeWaiterCancelToken)
: this()
Expand Down Expand Up @@ -191,6 +198,8 @@ private void OnLoaded(object sender, RoutedEventArgs e)
ListBox.DataContext = _viewModel;
ListBox.SuggestionsList.SelectionChanged += SuggestionsList_SelectionChanged;
ListBox.SuggestionsList.PreviewMouseLeftButtonUp += SuggestionsList_PreviewMouseLeftButtonUp;
ListBox.SuggestionsList.PreviewMouseLeftButtonDown += SuggestionsList_PreviewMouseLeftButtonDown;
ListBox.SuggestionsList.MouseMove += SuggestionsList_MouseMove;
_viewModel.PropertyChanged += ViewModel_PropertyChanged;
_viewModel.MainWindowVisibility = Visibility.Collapsed;
_viewModel.LoadedAtLeastOnce = true;
Expand Down Expand Up @@ -282,6 +291,48 @@ private void SuggestionsList_PreviewMouseLeftButtonUp(object sender, MouseButton
}
}

private void SuggestionsList_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_mouseDownPosition = e.GetPosition(null);
_mouseDownResultViewModel = ((FrameworkElement)e.OriginalSource).DataContext as ResultViewModel;
}

private void SuggestionsList_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && _mouseDownResultViewModel?.Result?.ContextData is IFileDropResult fileDropResult)
{
Vector dragDistance = _mouseDownPosition - e.GetPosition(null);
if (Math.Abs(dragDistance.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(dragDistance.Y) > SystemParameters.MinimumVerticalDragDistance)
{
_viewModel.Hide();

try
{
// DoDragDrop with file thumbnail as drag image
var dataObject = DragDataObject.FromFile(fileDropResult.Path);
using var bitmap = DragDataObject.BitmapSourceToBitmap((BitmapSource)_mouseDownResultViewModel?.Image);
IntPtr hBitmap = bitmap.GetHbitmap();

try
{
dataObject.SetDragImage(hBitmap, Constant.ThumbnailSize, Constant.ThumbnailSize);
DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy);
}
finally
{
Image.NativeMethods.DeleteObject(hBitmap);
}
}
catch
{
// DoDragDrop without drag image
IDataObject dataObject = new DataObject(DataFormats.FileDrop, new[] { fileDropResult.Path });
DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy);
}
}
}
}

private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.MainWindowVisibility))
Expand Down
14 changes: 14 additions & 0 deletions src/modules/launcher/Wox.Plugin/Interfaces/IFileDropResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 Wox.Plugin.Interfaces
{
/// <summary>
/// This interface is to indicate results that contain a file/folder that is available for drag & drop to other applications
/// </summary>
public interface IFileDropResult
{
public string Path { get; set; }
}
}
4 changes: 2 additions & 2 deletions src/modules/launcher/Wox.Test/Plugins/FolderPluginTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void ContextMenuLoaderReturnContextMenuForFolderWithOpenInConsoleWhenLoad
var mock = new Mock<IPublicAPI>();
var pluginInitContext = new PluginInitContext() { API = mock.Object };
var contextMenuLoader = new ContextMenuLoader(pluginInitContext);
var searchResult = new SearchResult() { Type = ResultType.Folder, FullPath = "C:/DummyFolder" };
var searchResult = new SearchResult() { Type = ResultType.Folder, Path = "C:/DummyFolder" };
var result = new Result() { ContextData = searchResult };

// Act
Expand All @@ -39,7 +39,7 @@ public void ContextMenuLoaderReturnContextMenuForFileWithOpenInConsoleWhenLoadCo
var mock = new Mock<IPublicAPI>();
var pluginInitContext = new PluginInitContext() { API = mock.Object };
var contextMenuLoader = new ContextMenuLoader(pluginInitContext);
var searchResult = new SearchResult() { Type = ResultType.File, FullPath = "C:/DummyFile.cs" };
var searchResult = new SearchResult() { Type = ResultType.File, Path = "C:/DummyFile.cs" };
var result = new Result() { ContextData = searchResult };

// Act
Expand Down

0 comments on commit 08d569c

Please sign in to comment.