laf icon indicating copy to clipboard operation
laf copied to clipboard

Introduce drag and drop handling

Open martincapello opened this issue 11 months ago • 41 comments

This PR contains the drag and drop implementation in each platform for laf. Also exposes the API that can be used by laf clients to use drag and drop in a platform-independent way.

martincapello avatar Mar 04 '24 21:03 martincapello

@dacap this is just to iterate over the design of the solution. This is not complete, just has the basic elements I come up with to draft a first approach to drag and drop handling. Let me know if you had something different in mind, because I also had another idea involving observers, but since we are not using observers in laf I had discarded that idea. The drag_and_drop.cpp file is a copy of multiple_windows.cpp example with a basic-empty implementation of the still-unfinished DnD API.

martincapello avatar Mar 04 '24 21:03 martincapello

Although it's not a bad idea to use these kind of delegates, I think it will make a little harder to process the events in the same order in Aseprite ui::Manager::generateMessagesFromOSEvents().

I think it would be better to keep all event handling as a queue of events (so we know the exact order of events).

dacap avatar Mar 04 '24 21:03 dacap

Actually my first approach was just about translating the event to laf as it is done for every other event. But (and maybe I should have mentioned this before) I ran into the following issue when trying to implement it for draggingUpdated:

This method is called periodically for the destination window when the user is dragging an "image" (as they call it on the docs) around. The destination window that implements the draggingUpdated must return the NSDragOperation value depending if it wants to accept the future drop or not, so when I realized that by creating a laf event and sending it to the queue it would be treated asynchronously, thus loosing the chance of returning the appropriate NSDragOperation at the requested time, I've decided to change the approach.

Also I believe the same issue exists when implementing IDropTarget::DragOver method for Windows.

Maybe there is a workaround for this that I'm not aware of, or I am missing something.

martincapello avatar Mar 05 '24 12:03 martincapello

It looks like a similar case to Window::handleHitTest or System::handleWindowResize (you will see a comment about a possible future SystemDelegate to replace handleWindowResize). These public std::function fields should be replaced in a future with some kind of delegate (probably everything in the window as a possible WindowDelegate).

We could offer some others handleDndSomething function fields but doing the delegate solution directly will be the best for the whole drag and drop mechanism. We can give a try and if it does work leave it as it is. (And if it doesn't work, then refactor it with events.)

I was also thinking that the drag and drop events will require several new fields that regular os::Events don't (and the queue of events is an array of os::Events), so probably it will make sense to just create a os::DndEvent for all these member functions of the delegate with all the drag and drop info required.

Some comments:

  1. Don't base your example on multiple_windows, drag_and_drop example could just have one window (make the example as small as possible to show only the drag and drop mechanism)
  2. Not sure if using DnD or Dnd as prefix or things like DragSource and DropTarget as class names for the delegates (same for the event, it could be os::DragEvent)
  3. The header file could be called os/dnd.h including everything needed for drag and drop
  4. os/os.h should include os/dnd.h so examples don't require to include this specific header
  5. I'm doing several refactors on the beta branch, this shouldn't be a problem as I'll merge/fix conflicts later with your changes

dacap avatar Mar 05 '24 13:03 dacap

It looks like a similar case to Window::handleHitTest or System::handleWindowResize (you will see a comment about a possible future SystemDelegate to replace handleWindowResize). These public std::function fields should be replaced in a future with some kind of delegate (probably everything in the window as a possible WindowDelegate).

Yeah, I saw those and I imagined that there were there because of a similar reason, in fact I've considered using functions instead of the delegates, but I think I saw the comment you mention so I decided to go for delegates at the end.

We could offer some others handleDndSomething function fields but doing the delegate solution directly will be the best for the whole drag and drop mechanism. We can give a try and if it does work leave it as it is. (And if it doesn't work, then refactor it with events.)

Ok, I'll go ahead with the proposed delegates then.

I was also thinking that the drag and drop events will require several new fields that regular os::Events don't (and the queue of events is an array of os::Events), so probably it will make sense to just create a os::DndEvent for all these member functions of the delegate with all the drag and drop info required.

Alright, will create a new os::DndEvent class that doesn't inherit from os::Event.

Some comments:

  1. Don't base your example on multiple_windows, drag_and_drop example could just have one window (make the example as small as possible to show only the drag and drop mechanism)

Sure, that is the idea. Making a copy of multiple_windows just was the quicker thing to do for me at the time.

  1. Not sure if using DnD or Dnd as prefix or things like DragSource and DropTarget as class names for the delegates (same for the event, it could be os::DragEvent)

Sure. We could also create a dnd namespace I guess, and use Source and Target as class names. But it is up to you, I don't have any preference. Just let me know if you want to go ahead with the names you proposed.

  1. The header file could be called os/dnd.h including everything needed for drag and drop

Sounds good.

  1. os/os.h should include os/dnd.h so examples don't require to include this specific header

Perfect.

  1. I'm doing several refactors on the beta branch, this shouldn't be a problem as I'll merge/fix conflicts later with your changes

👍

martincapello avatar Mar 05 '24 14:03 martincapello

@dacap This is still in draft but I want you to take a look at the current status. You can run the drag_and_drop example (which is not finished yet) to see some of things in action. Things implemented (just for macOS right now):

  • DragTarget definition.
  • Realtime drag feedback when the user moves the dragged data.
  • Support for dropping file paths from finder (this is similar as we have in the main branch, but using the DragTarget interface instead).
  • Introduction of DragDataProvider interface and its implementation for macOS. This is used to get data from the dragged payload, i.e. the Pasteboard in macOS.

Things missing:

  • DragSource definition.
  • Support for other kind of data items (images, text, pdf, RTF, strings, etc).
  • Changing the cursor at will.

However, I think that with the current status (and once we got this implemented in Windows and Linux) we have the basic blocks in place to start with https://github.com/aseprite/aseprite/issues/131 in Aseprite. Then we can implement the rest in laf in the future. What do you think?

martincapello avatar Mar 13 '24 14:03 martincapello

The first thing I tried was to drag this box and didn't work:

Screenshot 2024-03-13 at 14 26 15

I'll try to review this soon.

dacap avatar Mar 13 '24 17:03 dacap

Hehe...yeah, it can't be dragged...the DragSource is not implemented. So just try to drop files from finder...or any other DnD source app that provides NSFilenamesPboardType items.

martincapello avatar Mar 13 '24 17:03 martincapello

Probably we should create an example showing only the functionality we're implementing/need for the feature. Drop paths and images, nothing else.

dacap avatar Mar 13 '24 17:03 dacap

Yes, I agree. Once we are okay with the approach taken I will remove the things that are not functional from the example. Images are not supported at the moment. There are some commented code just to show how they could be approached, but nothing else.

martincapello avatar Mar 13 '24 17:03 martincapello

My idea was to implement the minimum necessary for supporting drag & dropping of files with realtime update of the UI while dragging them.

martincapello avatar Mar 13 '24 17:03 martincapello

About data types, we need only filenames/paths and images. Generally the "drag and drop" functions are related to the clipboard functions, so it might be possible that some functions from the clip library can be used (which might require changes in the clip library).

dacap avatar Mar 13 '24 18:03 dacap

One last time at the moment is if we can merge dropResult/acceptDrop in some way, or explain when each field must be set/used on each event (and if we need a DropEvent class to separate fields for a drop-only event).

dacap avatar Mar 13 '24 18:03 dacap

One last time at the moment is if we can merge dropResult/acceptDrop in some way, or explain when each field must be set/used on each event (and if we need a DropEvent class to separate fields for a drop-only event).

We can remove the acceptDrop field and let DragTarget::drop return a boolean that indicates if the drop was accepted or not. Something similar could be done for DragTarget::dragEnter and DragTarget::draggingUpdated by making them return a DropOperation and then removing the dropResult field from DragEvent.

martincapello avatar Mar 13 '24 20:03 martincapello

We can remove the acceptDrop field and let DragTarget::drop return a boolean that indicates if the drop was accepted or not. Something similar could be done for DragTarget::dragEnter and DragTarget::draggingUpdated by making them return a DropOperation and then removing the dropResult field from DragEvent.

They can be as they are, better to keep then as fields of the event. Probably the acceptDrop is confusing in the example because there is other acceptDrop (the one from the data structure).

I would try to keep the example as simple as possible (one window, removing the std::map, and with less option).

dacap avatar Mar 13 '24 21:03 dacap

About data types, we need only filenames/paths and images. Generally the "drag and drop" functions are related to the clipboard functions, so it might be possible that some functions from the clip library can be used (which might require changes in the clip library).

I've been looking the clip library, and yes, all the image-related functions could be adapted to reuse them in laf. Will see what can I do.

martincapello avatar Mar 15 '24 13:03 martincapello

Made the suggested changes and added the ability to get images from the dragged data. For this I copied and adapted code from the clip library (for macOs) to use laf surfaces. It was also needed to add some things to laf.

martincapello avatar Mar 20 '24 14:03 martincapello

Just pushed some progress on the windows implementation. It is not finished and also broke something in the macOs implementation because of some changes that I didn't have the chance to make today.

The example is half-working for windows, it lacks the dragged data reading/showing, right now it just shows the mouse position and receives the events appropriately.

martincapello avatar Mar 22 '24 21:03 martincapello

As we talked the other time, probably we can make the laf library depends on the clip library. I can prepare this change on aseprite and laf main branch moving the clip submodule from aseprite to laf.

dacap avatar Mar 26 '24 15:03 dacap

As we talked the other time, probably we can make the laf library depends on the clip library. I can prepare this change on aseprite and laf main branch moving the clip submodule from aseprite to laf.

Sure, but the thing was that the code in the clip library doesn't use os::Surface. So I think I will have to figure out how to interface from laf to the clip library without making clip aware of anything from laf.

martincapello avatar Mar 26 '24 15:03 martincapello

the thing was that the code in the clip library doesn't use os::Surface. So I think I will have to figure out how to interface from laf to the clip library without making clip aware of anything from laf.

Yes, but that shouldn't be a problem, as the clip library has a 32-bit image format with custom mask/shift (just like the os::Surface, the same pixel data could be used directly between clip/laf-os).

I'll try to move the submodule between the projects so we have clip already available in laf.

dacap avatar Mar 26 '24 16:03 dacap

The clip module is available in the main branch now.

dacap avatar Mar 26 '24 17:03 dacap

Oh, just in case, in the clip library there is another comptr implementation in clip_win_wic.h...which seems like incomplete or something (for instance it doesn't call AddRef() in any method).

martincapello avatar Mar 26 '24 17:03 martincapello

Oh, just in case, in the clip library there is another comptr implementation in clip_win_wic.h...which seems like incomplete or something (for instance it doesn't call AddRef() in any method).

The clip library is an independent library and has its own wrappers (comptr, coinit, hmodule, Hglobal) with all the methods that it needs. AFAIR as the clip comptr is a wrapper only to release created resources, there is no need for add ref (the reference is added by each creation functions, e.g. CoCreateInstance() adds the reference, and we then release it).

In the best case, we should avoid any interaction between laf <-> clip using COM pointer wrappers (just use raw pointers between boundaries if it's necessary, just like any other COM API).

dacap avatar Mar 26 '24 18:03 dacap

I fear that this PR might be impossible to review if gets to big. It might be interesting to think an approach were we merge first the macOS impl, then the Windows one, and finally the Linux one. (Even if some refactors to the events must be done for each impl.)

dacap avatar Apr 06 '24 01:04 dacap

I fear that this PR might be impossible to review if gets to big. It might be interesting to think an approach were we merge first the macOS impl, then the Windows one, and finally the Linux one. (Even if some refactors to the events must be done for each impl.)

Sure, no problem. But I think that first I would like to merge the windows implementation. Because I'm currently working on it along some clip lib changes (since now laf depends on clip). Then I will have to refactor some things of the macOS impl to make it uses clip (instead of the copy & paste I did before the clip dependency).

martincapello avatar Apr 08 '24 11:04 martincapello

Okay, I've organized a bit the commits history hopefully. These changes need this PR merged into aseprite fork: https://github.com/dacap/clip/pull/69

Also, the macOS implementation was removed for now, I have to make some adjustments and then I will commit it separately, maybe in a different PR after we've merged this.

There is an inconvenience about the windows implementation...to test the image drop in the drag_and_drop example you will have to use some application that is able to provide drag of clipboard formats such as "PNG", DIBV5, or DIB. I implemented a (basic and incomplete) win32 program for testing which allows the user to drag the last thing copied into the clipboard. If you want this program please let me know. In macOS this was different, I was able to use Chrome to drag images from the browser itself, the same doesn't happen in windows, it doesn't drag the image data in any of the format we use.

martincapello avatar Apr 11 '24 14:04 martincapello

Had to force push the last two commits to introduce the changes mentioned here: https://github.com/dacap/clip/pull/69#issuecomment-2050846596

martincapello avatar Apr 12 '24 18:04 martincapello

Sorry to force push again, but it really makes sense to me to have that clip submodule update within the windows implementation changes.

martincapello avatar Apr 12 '24 18:04 martincapello

I'll start reviewing this right now. So in some way I don't expect new commits until it's reviewed or merged.

dacap avatar Apr 15 '24 12:04 dacap