Drag and drop one of multiple stimuli to one of multiple locations

Hello,

I am very new to PsychoPy and coding in general, so sorry for how little I know. I am trying to create an experiment in which the participant will have to place 15 coloured squares in order (no correct/incorrect order). I have created the basis (in builder mode), with the 15 squares and 15 destination squares too. Next step is implementing the drag and drop feature, I am assuming to do this I will have to enter it as code.

I have tried to work it out and Frankenstein it with some of the solutions to similar queries on here but cannot for the life of me make it work…

Any suggestions or tips would be greatly appreciated.

Thanks.

Hi @Tom_Olive, all you need is a code component and mouse. Make sure your mouse settings is set to “never end routine on press”. Then, say you have shape called polygon and a mouse called mouse. You check whether the mouse isPressedIn the polygon object - see docs and if it is, you set the polygon position to the position of the mouse. Then you have drag and drop. E.g.,

if mouse.isPressedIn(polygon):
    polygon.setPos(mouse.getPos())

Thanks so much, do i need to do this 15 times, or can i just put:

if mouse.isPressedIn(Square1, Square2, Square3, etc.)

Sorry if these are obvious questions.

You are going to want to keep each conditional separate so that you only set the position for the shape that the mouse is pressed in.

Best to loop through your stimuli and have only one if/then check, and then exit the loop (otherwise, it is easy to end up dragging multiple stimuli as one passes over another). e.g.


for stimulus in [square01, square02, square03, etc]:
    if mouse.isPressedIn(stimulus):
        stimulus.pos = mouse.getPos()
        # exit the loop so only drag one stimulus at a time:
        break

Note the order in the list of stimuli should reflect the order in which they are drawn. Builder draws the stimulus components in order from top to bottom. So your list should be in the order as they appear in the Builder pane, but in reverse (i.e. from bottom to top). That way, your click will be detected by whatever stimulus is drawn on top of any other stimulus that might be at the same location. Otherwise, odd behaviour will occur (e.g. thinking you are clicking on a stimulus but finding that what moves is actually one that is hidden beneath it).

This code needs to be in the “each frame” tab of a code component, and the code component itself should appear above all of your stimulus components, so that they get affected immediately by any mouse movement.

1 Like

That’s great, thank you. And answers what was going to be one of my next queries.

Couple of other things I am trying to do…:

  • Any way I can make it so that the squares to be moved can only be dropped/placed on any of the destination squares?
  • I have no idea about PsychoPy data output yet, but all I will need is the information about which stimuli got placed in which destination square (something along the lines of: Dest1 = Square6, Dest2 = Square3, etc.).Is this something that would possible to be given at the end?

Sorry again for all the questions!

Edit:
Screenshot of what I’m working on to help make it clearer

Yes this is all possible. But it is an illustration of how often you can do something amazing with a single line of code (in this case, get real-time interactive animation of a stimulus) but the complexity of the code grows rapidly as you need to add more constraints.

I can’t write this code for you, as this really requires interactive testing which only you can do, but I can point you in the right direction. To figure this out, it is often useful to sketch out a flow chart, thinking of each action that the participant takes, and what information you need to track and what actions to take in response.

In this case, you not only need to track when the mouse button is pressed, but you also need to notice when it first becomes no longer pressed, but was pressed on the immediately preceding check. You also need to track what the last-clicked stimulus was, so when the mouse button is released, you then either bounce that stimulus back to its original location (which also needs to be stored), or if it is overlapping a destination, you snap it to align with it exactly. Some code needs to run at the start of the routine:

# Begin routine code:

square_locations = {} # an empty dictionary
for square in [square01, square02, square03, etc]:
    # associate each square with its original location:
    square_locations[square] = square.pos

clicked_square = None # initial status

and this is a sketch of the sort of structure you would need to call on each frame:

# each frame code

mouse_down = False

for square in [square01, square02, square03, etc]:
    if mouse.isPressedIn(square):
        square.pos = mouse.getPos() # animate the square
        clicked_square = square # remember which one is being clicked
        mouse_down = True
        # exit the loop so only drag one stimulus at a time:
        break

if not mouse_down and clicked_square is not None:
    # the mouse is no longer clicked but it previously was
    
    # so check if clicked_square overlaps each destination square.
    # if so, set its position to the exact position of that destination,
    # so it "snaps" to it.
    # shapeStims have a "contains" method, so you can check if the 
    # pos attribute of the square is contained within the boundary of
    # each destination:
    # https://www.psychopy.org/api/visual/shapestim.html
    # if not, then look up its original location in the square_locations
    # dictionary and send it back there
    # Also, store the name of the square  and the name of the 
    # associated destination in the data file, eg something like:
    thisExp.addData(square.name, destination.name)
  

    # lastly, don't forget this, so this check doesn't happen again
    # until the next time the mouse is released:
    clicked_square = None
1 Like

This is really helpful, thank you so much.

So far I’ve got this, which works on the click and drag but closes the trial as soon as I release the mouse click:

if not mouse_down and clicked_square is not None:
   for dest in [Dest1, Dest2, Dest3, Dest4, Dest5, Dest6, Dest7, Dest8, Dest9, Dest10, Dest11, Dest12, Dest13, Dest14, Dest15]:
      if dest.contains(clicked_square):
         square.pos = dest.pos
#      if not dest.contains(clicked_square):
#         square.pos = 
#   thisExp.addData(square.name, dest.name)
   clicked_square = None

The comments I have in there just to remind me of where I need to go. Unsure if that’s on the right track to look up the original locations…

Thank you.

Is there somewhere in your code where you call core.quit()? Otherwise, ensure that “end routine on press” is unchecked in your mouse component.

To look up the original locations, we have those stored in a dictionary and so I’m hoping that it would work to use the object to look up its stored location:

square.pos = square_locations[square]

If not, we might need to change things to use the names of the square stimuli in the dictionary instead, and look it up by that name, rather than the object itself.

As far as I can tell I haven’t got that in my code anywhere and the mouse option isn’t selected. This is the error message in the output window:

TypeError: only size-1 arrays can be converted to Python scalars

Sorry for the delay, I missed this reply, and so the issue may now be moot:

That error message is incomplete: there should be additional information pointing to exactly what line of code generates this error.

That’s okay, thanks for getting back to me.
Ahh, I couldn’t tell how much of the output was important. Although since the update it doesn’t even run at all…

Error message:

 Traceback (most recent call last):
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\psychopy\gui\qtgui.py", line 15, in <module>
    from PyQt4 import QtGui  
ModuleNotFoundError: No module named 'PyQt4'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\Tom\Documents\4 - Work\Uni\Pschology\3rd Year\0 - Dissertation\PsychoPy\Lanthony D-15 v1_lastrun.py", line 14, in <module>
    from psychopy import locale_setup, sound, gui, visual, core, data, event, logging, clock
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\psychopy\gui\__init__.py", line 33, in <module>
    from .qtgui import *
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\psychopy\gui\qtgui.py", line 19, in <module>
    from PyQt5 import QtWidgets
ImportError: DLL load failed: The specified procedure could not be found.

If that helps at all…

Thanks.

OK this is an issue with the packages distributed within the PsychoPy application itself. I don’t know what version you are using, but you should watch this thread for progress:

Ahh thanks for the tip. Would you think it would be worth re-downloading the previous version in the meantime to continue working on it?

I’ve gone back to v3.0.0, I’ve been told that’s the version of PsychoPy running on the lab computers anyway, best to work from that anyway.
The full error message I was trying to post before is this:

  File "C:\Users\Tom\Documents\4 - Work\Uni\Pschology\3rd Year\0 - Dissertation\PsychoPy\Lanthony D-15 v1_lastrun.py", line 448, in <module>
    if dest.contains(clicked_square):
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\psychopy\visual\basevisual.py", line 591, in contains
    return pointInPolygon(xy[0], xy[1], poly=poly)
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\psychopy\visual\helpers.py", line 67, in pointInPolygon
    return mplPath(poly).contains_point([x, y])
  File "C:\Program Files (x86)\PsychoPy3\lib\site-packages\matplotlib\path.py", line 488, in contains_point
    return _path.point_in_path(point[0], point[1], radius, self, transform)
TypeError: only size-1 arrays can be converted to Python scalars