Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce drag and drop handling #83

Merged
merged 39 commits into from
May 30, 2024
Merged

Introduce drag and drop handling #83

merged 39 commits into from
May 30, 2024

Conversation

martincapello
Copy link
Member

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 martincapello requested a review from dacap March 4, 2024 21:00
Copy link
Collaborator

@aseprite-bot aseprite-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

os::draw_text(s, nullptr, buf, gfx::Point(0, y), &paint);
y += 12;

for (auto line : lines) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'auto line' can be declared as 'const auto *line' [readability-qualified-auto]

Suggested change
for (auto line : lines) {
for (const auto *line : lines) {

{
// If all windows are hidden, show them again
auto hidden = std::count_if(windows.begin(), windows.end(),
[](os::WindowRef window){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the parameter 'window' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]

Suggested change
[](os::WindowRef window){
[](const os::WindowRef& window){

});
if (hidden == windows.size()) {
std::for_each(windows.begin(), windows.end(),
[](os::WindowRef window){
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: the parameter 'window' is copied for each invocation but only used as a const reference; consider making it a const reference [performance-unnecessary-value-param]

Suggested change
[](os::WindowRef window){
[](const os::WindowRef& window){

examples/drag_and_drop.cpp Show resolved Hide resolved
examples/drag_and_drop.cpp Show resolved Hide resolved
os::ScreenList screens;
system->listScreens(screens);
char chr = 'A';
for (os::ScreenRef& screen : screens) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'screen' of type 'os::ScreenRef &' (aka 'Refos::Screen &') can be declared 'const' [misc-const-correctness]

Suggested change
for (os::ScreenRef& screen : screens) {
for (os::ScreenRef const& screen : screens) {

for (auto& p : pos) {
os::WindowSpec s = spec;
gfx::Rect frame = s.frame();
frame.x += frame.w*p.x;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'double' to 'int' [bugprone-narrowing-conversions]

      frame.x += frame.w*p.x;
                 ^

os::WindowSpec s = spec;
gfx::Rect frame = s.frame();
frame.x += frame.w*p.x;
frame.y += frame.h*p.y;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: narrowing conversion from 'double' to 'int' [bugprone-narrowing-conversions]

      frame.y += frame.h*p.y;
                 ^


// Duplicate window
case os::kKeyD: {
std::string title = ev.window()->title();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'title' of type 'std::string' (aka 'basic_string') can be declared 'const' [misc-const-correctness]

Suggested change
std::string title = ev.window()->title();
std::string const title = ev.window()->title();

case os::kKeyF:
case os::kKeyC:
case os::kKeyW: {
std::string title = ev.window()->title();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'title' of type 'std::string' (aka 'basic_string') can be declared 'const' [misc-const-correctness]

Suggested change
std::string title = ev.window()->title();
std::string const title = ev.window()->title();

@martincapello
Copy link
Member Author

@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.

@dacap
Copy link
Member

dacap commented Mar 4, 2024

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).

@martincapello
Copy link
Member Author

martincapello commented Mar 5, 2024

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.

@dacap
Copy link
Member

dacap commented Mar 5, 2024

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

@martincapello
Copy link
Member Author

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 martincapello force-pushed the dnd branch 3 times, most recently from 66f9381 to 8408f84 Compare March 5, 2024 20:52
Copy link
Collaborator

@aseprite-bot aseprite-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

case os::Event::KeyDown:
switch (ev.scancode()) {

case os::kKeyT:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: switch has 2 consecutive identical branches [bugprone-branch-clone]

          case os::kKeyT:
          ^
Additional context

examples/drag_and_drop.cpp:151: last of these clones ends here

            break;
                 ^

Copy link
Collaborator

@aseprite-bot aseprite-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

case os::Event::KeyDown:
switch (ev.scancode()) {

case os::kKeyT:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: switch has 2 consecutive identical branches [bugprone-branch-clone]

          case os::kKeyT:
          ^
Additional context

examples/drag_and_drop.cpp:203: last of these clones ends here

            break;
                 ^

Copy link
Collaborator

@aseprite-bot aseprite-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

case os::Event::KeyDown:
switch (ev.scancode()) {

case os::kKeyT:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: switch has 2 consecutive identical branches [bugprone-branch-clone]

          case os::kKeyT:
          ^
Additional context

examples/drag_and_drop.cpp:221: last of these clones ends here

            break;
                 ^

Copy link
Collaborator

@aseprite-bot aseprite-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

case os::Event::KeyDown:
switch (ev.scancode()) {

case os::kKeyT:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: switch has 2 consecutive identical branches [bugprone-branch-clone]

          case os::kKeyT:
          ^
Additional context

examples/drag_and_drop.cpp:232: last of these clones ends here

            break;
                 ^

@martincapello
Copy link
Member Author

@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 aseprite/aseprite#131 in Aseprite. Then we can implement the rest in laf in the future. What do you think?

@dacap
Copy link
Member

dacap commented Mar 13, 2024

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.

@martincapello
Copy link
Member Author

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.

@dacap
Copy link
Member

dacap commented Mar 13, 2024

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

@martincapello
Copy link
Member Author

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
Copy link
Member Author

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

Copy link
Member

@dacap dacap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far it looks good, some minor changes to the API and still some decisions to take (e.g. should we use unique pointers for targets/sources or leave the owning decision to the client side).

The example could be simplified just showing a box where we can drop things (files/images) and accept or not the DragEvent if we drop things inside that box (to avoid switching flags so many flags).

os/window.h Show resolved Hide resolved
os/window.h Outdated Show resolved Hide resolved
os/window.h Outdated Show resolved Hide resolved
os/dnd.h Outdated Show resolved Hide resolved
os/dnd.h Show resolved Hide resolved
os/dnd.h Show resolved Hide resolved
os/dnd.h Outdated Show resolved Hide resolved
os/dnd.h Outdated Show resolved Hide resolved
@dacap
Copy link
Member

dacap commented Mar 13, 2024

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
Copy link
Member

dacap commented Mar 13, 2024

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 dacap assigned martincapello and unassigned dacap Mar 13, 2024
@martincapello
Copy link
Member Author

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 and others added 23 commits May 9, 2024 09:22
…om a source image that doesn't support alpha
…mages and offer the possibility of configuring the decoding functions to be used

This is achieved by looking at the clipboard for a file that contains data in some of the supported image formats, because in Windows, the Chrome implementation puts the image's file content in the clipboard. Then after determining that the file contents corresponds to one of our supported image formats, the configured decoding function is used (which might be the default one)
This is how most examples are working right now, draw and invalidate
the updated area in the same function just to avoid calling
invalidate() each time.
We need drag & drop working with scale parameter (as Aseprite uses
scale=2 by default), so I've made some changes to test:

1) draw_window() must use the surface bounds which are scaled (not
   the windows bounds, which aren't)
2) The drop zone must use the same scaled bounds
3) Moved all logic to create the window inside create_window()
…f flags instead of any of the specified flags
@martincapello martincapello assigned dacap and unassigned martincapello May 9, 2024
@dacap dacap merged commit cb15528 into main May 30, 2024
13 checks passed
@dacap
Copy link
Member

dacap commented May 30, 2024

Good job @martincapello! merged just right now 🍾

@dacap dacap deleted the dnd branch May 30, 2024 14:19
@martincapello
Copy link
Member Author

Good job @martincapello! merged just right now 🍾

😅 tough one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants