PowerToys icon indicating copy to clipboard operation
PowerToys copied to clipboard

[PTRun] Drag and drop files

Open daniel-richter opened this issue 2 years ago • 11 comments

Summary of the Pull Request

Support drag&drop to other application for files in result list. Recording 2022-12-01 at 20 03 27

PR Checklist

  • [x] Closes: #4462
  • [ ] Communication: I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected
  • [ ] Dev docs: Added/updated

Detailed Description of the Pull Request / Additional comments

To implement this behaviour, I added the following things:

  • detect dragging an item from the suggestions list
  • check whether this item represents a file/folder (something path-related)
  • perform a DragDrop with path as FileDrop
  • make related plugins (that return files/folders) mark their results as draggable file/folder

detect dragging an item from the suggestions list

extend src/modules/launcher/PowerLauncher/MainWindow.xaml.cs Save mouse position on mouse down and check aggainst system drag distance

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 && ...)
    {
        Vector dragDistance = _mouseDownPosition - e.GetPosition(null);
        if (Math.Abs(dragDistance.X) > SystemParameters.MinimumHorizontalDragDistance || Math.Abs(dragDistance.Y) > SystemParameters.MinimumVerticalDragDistance)
        {
            ...
       }
    }
}

check whether this item represents a file/folder (something path-related)

Since all plugins that return files/folders put their data objects containing the path information into Result.ContextData, I decided to introduce an interface IFileDropResult to indicate that a result contains a file/folder to be draggable. src/modules/launcher/Wox.Plugin/Interfaces/IFileDropResult.cs

public interface IFileDropResult
{
    public string Path { get; set; }
}

So we can check whether the result connected to the view model of the clicked result item implements this interface.

private void SuggestionsList_MouseMove(object sender, MouseEventArgs e)
{
    if (... && _mouseDownResultViewModel?.Result?.ContextData is IFileDropResult fileDropResult)
    {  ...  }
}

perform a DragDrop with path as FileDrop

If so, the PTRun window will be hidden and the corresponding path of the dragged item will be made available for dragging as FileDrop:

_viewModel.Hide();
DataObject dataObject = new DataObject(DataFormats.FileDrop, new[] { fileDropResult.Path });
DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy);

make related plugins (that return files/folders) mark their results as draggable file/folder

The search result types of the following plugins now implement IFileDropResult:

For unification, I had to rename Folder/SearchResult's FullPath to Path.

Validation Steps Performed

(See screen recording)

Recording 2022-12-01 at 19 59 50

daniel-richter avatar Dec 01 '22 19:12 daniel-richter

Is this feature only for folder plugin? It might make sense to have it in the indexer plugin too.

htcfreek avatar Dec 01 '22 22:12 htcfreek

As already stated above, this feature is available for the following plugins:

daniel-richter avatar Dec 01 '22 22:12 daniel-richter

As already stated above, this feature is available for the following plugins:

But is it correct that the everything plugin needs to be updated to implement the new interface?

htcfreek avatar Dec 01 '22 22:12 htcfreek

Yes. But I can't do it without having this changes in PTRun first. :-)

daniel-richter avatar Dec 01 '22 23:12 daniel-richter

Do we have a check if the access to the path is denied. We should prevent a crash in this case.

htcfreek avatar Dec 02 '22 12:12 htcfreek

Do we have a check if the access to the path is denied. We should prevent a crash in this case.

Hm, not sure what to do... The Drag&Drop does not process "the file" in any way but only passes the path (string) as data to the shell. So DoDragDrop should not fail, because what the target does with the path is the responsibility of the drop target. The transfer of the data (path as string) itself is successful if a target accepts the drop. A drop target application does not even have to open access the path but only process the provided path in some way (e.g. add to text).

Open questions:

  1. Should return value of DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy); be checked in any way? DoDragDrop will block the execution until drag&drop is finished - the result value is DragDropEffects.Copy if some drop target accepted the drop or DragDropEffects.None if there wasn't. Maybe make the search results visible aggain if there was no drop?

  2. An alternative implementation to indicate file/folder search results (current proposal: a special type (IFileDropResult) for the Result.ContextData property) is to make the file/folder path a first class property of Result. Would that be more elgant?

  3. I have a working solution for adding the file thumbnail as drop image. Should I include that into this pull request?

daniel-richter avatar Dec 02 '22 14:12 daniel-richter

Do we have a check if the access to the path is denied. We should prevent a crash in this case.

Hm, not sure what to do... The Drag&Drop does not process "the file" in any way but only passes the path (string) as data to the shell. So DoDragDrop should not fail, because what the target does with the path is the responsibility of the drop target. The transfer of the data (path as string) itself is successful if a target accepts the drop. A drop target application does not even have to open access the path but only process the provided path in some way (e.g. add to text).

Open questions:

  1. Should return value of DragDrop.DoDragDrop(ListBox.SuggestionsList, dataObject, DragDropEffects.Copy); be checked in any way? DoDragDrop will block the execution until drag&drop is finished - the result value is DragDropEffects.Copy if some drop target accepted the drop or DragDropEffects.None if there wasn't. Maybe make the search results visible aggain if there was no drop?

Regarding the error check: If we can't crash or hang I see no reason to handle something in our code.

  1. An alternative implementation to indicate file/folder search results (current proposal: a special type (IFileDropResult) for the Result.ContextData property) is to make the file/folder path a first class property of Result. Would that be more elgant?

Not sure. But it shouldn't be to complicated.

  1. I have a working solution for adding the file thumbnail as drop image. Should I include that into this pull request?

Regarding the thumbnail: Do you have a screenshot of what you mean?

htcfreek avatar Dec 02 '22 14:12 htcfreek

no drag image with file thumbnail as drag image
Recording 2022-12-02 at 16 16 17 Recording 2022-12-02 at 16 18 16

Note: Displayingd the drag image is also the responsibility of the drop target. In the shown example it's Windows Explorer.

daniel-richter avatar Dec 02 '22 15:12 daniel-richter

no drag image with file thumbnail as drag image
Recording 2022-12-02 at 16 16 17 Recording 2022-12-02 at 16 18 16

Note: Displayingd the drag image is also the responsibility of the drop target. In the shown example it's Windows Explorer.

Looks good. Let's push it.

htcfreek avatar Dec 02 '22 15:12 htcfreek

@check-spelling-bot Report

:red_circle: Please review

See the :open_file_folder: files view or the :scroll:action log for details.

Unrecognized words (1)

pshdi

To accept :heavy_check_mark: these unrecognized words as correct, run the following commands

... in a clone of the [email protected]:daniel-richter/PowerToys-FileDrop.git repository on the main branch (:information_source: how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.21/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/3603301370/attempts/1'
Available :books: dictionaries could cover words not in the :blue_book: dictionary

This includes both expected items (2138) from .github/actions/spell-check/expect.txt and unrecognized words (1)

Dictionary Entries Covers
cspell:cpp/src/cpp.txt 30216 121
cspell:win32/src/win32.txt 53509 118
cspell:python/src/python/python-lib.txt 3873 30
cspell:php/php.txt 2597 16
cspell:node/node.txt 1768 14
cspell:typescript/typescript.txt 1211 12
cspell:java/java.txt 7642 11
cspell:python/src/python/python.txt 453 10
cspell:r/src/r.txt 808 8
cspell:python/src/common/extra.txt 741 7

Consider adding them using (in .github/workflows/spelling2.yml):

      with:
        extra_dictionaries:
          cspell:cpp/src/cpp.txt
          cspell:win32/src/win32.txt
          cspell:python/src/python/python-lib.txt
          cspell:php/php.txt
          cspell:node/node.txt
          cspell:typescript/typescript.txt
          cspell:java/java.txt
          cspell:python/src/python/python.txt
          cspell:r/src/r.txt
          cspell:python/src/common/extra.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
If the flagged items are :exploding_head: false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it, try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

github-actions[bot] avatar Dec 02 '22 16:12 github-actions[bot]

Looks good. Let's push it.

Done. Changed DoDragDrop to:

_viewModel.Hide();

try
{
    // DoDragDrop with file thumbnail as drag image
    var dataObject = DragDataObject.FromFile(fileDropResult.Path);
    IntPtr hBitmap = Image.WindowsThumbnailProvider.GetHBitmap(Path.GetFullPath(fileDropResult.Path), Constant.ThumbnailSize, Constant.ThumbnailSize, Image.ThumbnailOptions.None);
    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);
}

I added src/modules/launcher/PowerLauncher/Helper/DragDataObject.cs with helper methods to deal with shell data objects (and drag images) and had to make Image.WindowsThumbnailProvider.GetHBitmap public (because a HBitmap/IntPtr is needed for the thumbnail image).

daniel-richter avatar Dec 02 '22 16:12 daniel-richter

@daniel-richter When do we create the hBitmap? Can this make search or dragging slow?

htcfreek avatar Dec 05 '22 14:12 htcfreek

When do we create the hBitmap? Can this make search or dragging slow?

It's done before starting the drag&drop action (after clicking one result item and dragging the mouse the minumum drag distance). So drag&drop function won't affect searching in any way.

Creating the hBitmap could be slow, but the Folder and Indexer plugins will set the IconPath to the file/folder, so the thumbnails will be loaded anyway. (The Everything plugin has a setting do diable preview.)

The thumbnails for search results will be cached (ImageSource Wox.Infrastructure.Image.ImageLoader.Load). Also, the drag icon itself is already present in PowerLauncher.ViewModel.ResultViewModel.Image resp. _mouseDownResultViewModel.Image. But I couldn't find a nice way get an hBitmap from an ImageSource typed object. If that could be done, we could simply reuse the already loaded icon from the search result.

daniel-richter avatar Dec 05 '22 15:12 daniel-richter

@check-spelling-bot Report

:red_circle: Please review

See the :open_file_folder: files view or the :scroll:action log for details.

Unrecognized words (2)

bgr weblog

Previously acknowledged words that are now absent BGR :arrow_right:
To accept :heavy_check_mark: these unrecognized words as correct and remove the previously acknowledged and now absent words, run the following commands

... in a clone of the [email protected]:daniel-richter/PowerToys-FileDrop.git repository on the main branch (:information_source: how do I use this?):

curl -s -S -L 'https://raw.githubusercontent.com/check-spelling/check-spelling/v0.0.21/apply.pl' |
perl - 'https://github.com/microsoft/PowerToys/actions/runs/3621892784/attempts/1'
Available :books: dictionaries could cover words not in the :blue_book: dictionary

This includes both expected items (2138) from .github/actions/spell-check/expect.txt and unrecognized words (2)

Dictionary Entries Covers
cspell:cpp/src/cpp.txt 30216 122
cspell:win32/src/win32.txt 53509 117
cspell:python/src/python/python-lib.txt 3873 30
cspell:php/php.txt 2597 16
cspell:node/node.txt 1768 14
cspell:typescript/typescript.txt 1211 12
cspell:java/java.txt 7642 11
cspell:python/src/python/python.txt 453 10
cspell:r/src/r.txt 808 8
cspell:python/src/common/extra.txt 741 7

Consider adding them using (in .github/workflows/spelling2.yml):

      with:
        extra_dictionaries:
          cspell:cpp/src/cpp.txt
          cspell:win32/src/win32.txt
          cspell:python/src/python/python-lib.txt
          cspell:php/php.txt
          cspell:node/node.txt
          cspell:typescript/typescript.txt
          cspell:java/java.txt
          cspell:python/src/python/python.txt
          cspell:r/src/r.txt
          cspell:python/src/common/extra.txt

To stop checking additional dictionaries, add:

      with:
        check_extra_dictionaries: ''
If the flagged items are :exploding_head: false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it, try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

github-actions[bot] avatar Dec 05 '22 16:12 github-actions[bot]

[...] the drag icon itself is already present in PowerLauncher.ViewModel.ResultViewModel.Image resp. _mouseDownResultViewModel.Image. But I couldn't find a nice way get an hBitmap from an ImageSource typed object. If that could be done, we could simply reuse the already loaded icon from the search result.

Okay, found a solution in https://stackoverflow.com/a/2897325 Now, PowerLauncher.ViewModel.ResultViewModel.Image is (re)used for the drag image.

daniel-richter avatar Dec 05 '22 16:12 daniel-richter